mirror of
https://github.com/salesagility/SuiteCRM.git
synced 2024-11-22 07:52:36 +00:00
3940 lines
148 KiB
JavaScript
3940 lines
148 KiB
JavaScript
// version: 2015-11-02
|
|
/**
|
|
* o--------------------------------------------------------------------------------o
|
|
* | This file is part of the RGraph package - you can learn more at: |
|
|
* | |
|
|
* | http://www.rgraph.net |
|
|
* | |
|
|
* | RGraph is dual licensed under the Open Source GPL (General Public License) |
|
|
* | v2.0 license and a commercial license which means that you're not bound by |
|
|
* | the terms of the GPL. The commercial license is just £99 (GBP) and you can |
|
|
* | read about it here: |
|
|
* | http://www.rgraph.net/license |
|
|
* o--------------------------------------------------------------------------------o
|
|
*/
|
|
|
|
RGraph = window.RGraph || {isRGraph: true};
|
|
|
|
/**
|
|
* The line chart constructor
|
|
*
|
|
* @param object canvas The cxanvas object
|
|
* @param array ... The lines to plot
|
|
*/
|
|
RGraph.Line = function (conf)
|
|
{
|
|
/**
|
|
* Allow for object config style
|
|
*/
|
|
if ( typeof conf === 'object'
|
|
&& typeof conf.data === 'object'
|
|
&& typeof conf.id === 'string') {
|
|
|
|
var id = conf.id;
|
|
var canvas = document.getElementById(id);
|
|
var data = conf.data;
|
|
var parseConfObjectForOptions = true; // Set this so the config is parsed (at the end of the constructor)
|
|
|
|
} else {
|
|
|
|
var id = conf;
|
|
var canvas = document.getElementById(id);
|
|
var data = arguments[1];
|
|
}
|
|
|
|
|
|
|
|
|
|
this.id = id;
|
|
this.canvas = canvas;
|
|
this.context = this.canvas.getContext('2d');
|
|
this.canvas.__object__ = this;
|
|
this.type = 'line';
|
|
this.max = 0;
|
|
this.coords = [];
|
|
this.coords2 = [];
|
|
this.coords.key = [];
|
|
this.coordsText = [];
|
|
this.coordsSpline = [];
|
|
this.hasnegativevalues = false;
|
|
this.isRGraph = true;
|
|
this.uid = RGraph.CreateUID();
|
|
this.canvas.uid = this.canvas.uid ? this.canvas.uid : RGraph.CreateUID();
|
|
this.colorsParsed = false;
|
|
this.original_colors = [];
|
|
this.firstDraw = true; // After the first draw this will be false
|
|
|
|
|
|
/**
|
|
* Compatibility with older browsers
|
|
*/
|
|
//RGraph.OldBrowserCompat(this.context);
|
|
|
|
|
|
// Various config type stuff
|
|
this.properties =
|
|
{
|
|
'chart.background.barcolor1': 'rgba(0,0,0,0)',
|
|
'chart.background.barcolor2': 'rgba(0,0,0,0)',
|
|
'chart.background.grid': 1,
|
|
'chart.background.grid.width': 1,
|
|
'chart.background.grid.hsize': 25,
|
|
'chart.background.grid.vsize': 25,
|
|
'chart.background.grid.color': '#ddd',
|
|
'chart.background.grid.vlines': true,
|
|
'chart.background.grid.hlines': true,
|
|
'chart.background.grid.border': true,
|
|
'chart.background.grid.autofit': true,
|
|
'chart.background.grid.autofit.align': true,
|
|
'chart.background.grid.autofit.numhlines': 5,
|
|
'chart.background.grid.autofit.numvlines': null,
|
|
'chart.background.grid.dashed': false,
|
|
'chart.background.grid.dotted': false,
|
|
'chart.background.hbars': null,
|
|
'chart.background.image': null,
|
|
'chart.background.image.stretch': true,
|
|
'chart.background.image.x': null,
|
|
'chart.background.image.y': null,
|
|
'chart.background.image.w': null,
|
|
'chart.background.image.h': null,
|
|
'chart.background.image.align': null,
|
|
'chart.background.color': null,
|
|
'chart.labels': null,
|
|
'chart.labels.bold': false,
|
|
'chart.labels.color': null,
|
|
'chart.labels.ingraph': null,
|
|
'chart.labels.above': false, // Working
|
|
'chart.labels.above.size': 8, // Working
|
|
'chart.labels.above.decimals': null, // Working
|
|
'chart.labels.above.color': null,
|
|
'chart.labels.above.background': 'white',
|
|
'chart.labels.above.font': null,
|
|
'chart.labels.above.border': true,
|
|
'chart.labels.above.offsety': 5,
|
|
'chart.labels.above.units.pre': '',
|
|
'chart.labels.above.units.post': '',
|
|
'chart.labels.above.specific': null,
|
|
'chart.xtickgap': 20,
|
|
'chart.smallxticks': 3,
|
|
'chart.largexticks': 5,
|
|
'chart.ytickgap': 20,
|
|
'chart.smallyticks': 3,
|
|
'chart.largeyticks': 5,
|
|
'chart.numyticks': 10,
|
|
'chart.linewidth': 2.01,
|
|
'chart.colors': ['red', '#0f0', '#00f', '#f0f', '#ff0', '#0ff','green','pink','blue','black'],
|
|
'chart.hmargin': 0,
|
|
'chart.tickmarks.dot.stroke': 'white',
|
|
'chart.tickmarks.dot.fill': null,
|
|
'chart.tickmarks.dot.linewidth': 3,
|
|
'chart.tickmarks': 'endcircle',
|
|
'chart.tickmarks.linewidth': null,
|
|
'chart.tickmarks.image': null,
|
|
'chart.tickmarks.image.halign': 'center',
|
|
'chart.tickmarks.image.valign': 'center',
|
|
'chart.tickmarks.image.offsetx':0,
|
|
'chart.tickmarks.image.offsety':0,
|
|
'chart.ticksize': 3,
|
|
'chart.gutter.left': 25,
|
|
'chart.gutter.right': 25,
|
|
'chart.gutter.top': 25,
|
|
'chart.gutter.bottom': 30,
|
|
'chart.tickdirection': -1,
|
|
'chart.yaxispoints': 5,
|
|
'chart.fillstyle': null,
|
|
'chart.xaxispos': 'bottom',
|
|
'chart.yaxispos': 'left',
|
|
'chart.xticks': null,
|
|
'chart.text.size': 12,
|
|
'chart.text.angle': 0,
|
|
'chart.text.color': 'black',
|
|
'chart.text.font': 'Arial',
|
|
'chart.ymin': 0,
|
|
'chart.ymax': null,
|
|
'chart.title': '',
|
|
'chart.title.background': null,
|
|
'chart.title.hpos': null,
|
|
'chart.title.vpos': null,
|
|
'chart.title.bold': true,
|
|
'chart.title.font': null,
|
|
'chart.title.xaxis': '',
|
|
'chart.title.xaxis.bold': true,
|
|
'chart.title.xaxis.size': null,
|
|
'chart.title.xaxis.font': null,
|
|
'chart.title.yaxis': '',
|
|
'chart.title.yaxis.bold': true,
|
|
'chart.title.yaxis.size': null,
|
|
'chart.title.yaxis.font': null,
|
|
'chart.title.yaxis.color': null,
|
|
'chart.title.xaxis.pos': null,
|
|
'chart.title.yaxis.pos': null,
|
|
'chart.title.yaxis.x': null,
|
|
'chart.title.yaxis.y': null,
|
|
'chart.title.xaxis.x': null,
|
|
'chart.title.xaxis.y': null,
|
|
'chart.title.x': null,
|
|
'chart.title.y': null,
|
|
'chart.title.halign': null,
|
|
'chart.title.valign': null,
|
|
'chart.shadow': true,
|
|
'chart.shadow.offsetx': 2,
|
|
'chart.shadow.offsety': 2,
|
|
'chart.shadow.blur': 3,
|
|
'chart.shadow.color': 'rgba(128,128,128,0.5)',
|
|
'chart.tooltips': null,
|
|
'chart.tooltips.hotspot.xonly': false,
|
|
'chart.tooltips.hotspot.size': 5,
|
|
'chart.tooltips.effect': 'fade',
|
|
'chart.tooltips.css.class': 'RGraph_tooltip',
|
|
'chart.tooltips.event': 'onmousemove',
|
|
'chart.tooltips.highlight': true,
|
|
'chart.tooltips.coords.page': false,
|
|
'chart.highlight.stroke': 'gray',
|
|
'chart.highlight.fill': 'white',
|
|
'chart.stepped': false,
|
|
'chart.key': null,
|
|
'chart.key.background': 'white',
|
|
'chart.key.position': 'graph',
|
|
'chart.key.halign': null,
|
|
'chart.key.shadow': false,
|
|
'chart.key.shadow.color': '#666',
|
|
'chart.key.shadow.blur': 3,
|
|
'chart.key.shadow.offsetx': 2,
|
|
'chart.key.shadow.offsety': 2,
|
|
'chart.key.position.gutter.boxed': false,
|
|
'chart.key.position.x': null,
|
|
'chart.key.position.y': null,
|
|
'chart.key.color.shape': 'square',
|
|
'chart.key.rounded': true,
|
|
'chart.key.linewidth': 1,
|
|
'chart.key.colors': null,
|
|
'chart.key.interactive': false,
|
|
'chart.key.interactive.highlight.chart.stroke': 'rgba(255,0,0,0.3)',
|
|
'chart.key.interactive.highlight.label': 'rgba(255,0,0,0.2)',
|
|
'chart.key.text.color': 'black',
|
|
'chart.contextmenu': null,
|
|
'chart.ylabels': true,
|
|
'chart.ylabels.count': 5,
|
|
'chart.ylabels.inside': false,
|
|
'chart.scale.invert': false,
|
|
'chart.xlabels.inside': false,
|
|
'chart.xlabels.inside.color': 'rgba(255,255,255,0.5)',
|
|
'chart.noaxes': false,
|
|
'chart.noyaxis': false,
|
|
'chart.noxaxis': false,
|
|
'chart.noendxtick': false,
|
|
'chart.noendytick': false,
|
|
'chart.units.post': '',
|
|
'chart.units.pre': '',
|
|
'chart.scale.zerostart': false,
|
|
'chart.scale.decimals': null,
|
|
'chart.scale.point': '.',
|
|
'chart.scale.thousand': ',',
|
|
'chart.crosshairs': false,
|
|
'chart.crosshairs.color': '#333',
|
|
'chart.crosshairs.hline': true,
|
|
'chart.crosshairs.vline': true,
|
|
'chart.annotatable': false,
|
|
'chart.annotate.color': 'black',
|
|
'chart.axesontop': false,
|
|
'chart.filled': false,
|
|
'chart.filled.range': false,
|
|
'chart.filled.range.threshold': null,
|
|
'chart.filled.range.threshold.colors': ['red', 'green'],
|
|
'chart.filled.accumulative': true,
|
|
'chart.variant': null,
|
|
'chart.axis.color': 'black',
|
|
'chart.axis.linewidth': 1,
|
|
'chart.numxticks': (data && typeof(data[0]) == 'number' ? data.length - 1: 20),
|
|
'chart.numyticks': 10,
|
|
'chart.zoom.factor': 1.5,
|
|
'chart.zoom.fade.in': true,
|
|
'chart.zoom.fade.out': true,
|
|
'chart.zoom.hdir': 'right',
|
|
'chart.zoom.vdir': 'down',
|
|
'chart.zoom.frames': 25,
|
|
'chart.zoom.delay': 16.666,
|
|
'chart.zoom.shadow': true,
|
|
'chart.zoom.background': true,
|
|
'chart.zoom.action': 'zoom',
|
|
'chart.backdrop': false,
|
|
'chart.backdrop.size': 30,
|
|
'chart.backdrop.alpha': 0.2,
|
|
'chart.resizable': false,
|
|
'chart.resize.handle.adjust': [0,0],
|
|
'chart.resize.handle.background': null,
|
|
'chart.adjustable': false,
|
|
'chart.noredraw': false,
|
|
'chart.outofbounds': false,
|
|
'chart.chromefix': true,
|
|
'chart.animation.factor': 1,
|
|
'chart.animation.unfold.x': false,
|
|
'chart.animation.unfold.y': true,
|
|
'chart.animation.unfold.initial': 2,
|
|
'chart.animation.trace.clip': 1,
|
|
'chart.curvy': false,
|
|
'chart.line.visible': true,
|
|
'chart.events.click': null,
|
|
'chart.events.mousemove': null
|
|
}
|
|
|
|
/**
|
|
* Change null arguments to empty arrays
|
|
*/
|
|
for (var i=1; i<arguments.length; ++i) {
|
|
if (typeof(arguments[i]) == 'null' || !arguments[i]) {
|
|
arguments[i] = [];
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Store the original data. This also allows for giving arguments as one big array.
|
|
*/
|
|
this.original_data = [];
|
|
|
|
// This allows for the new object based configuration style
|
|
if (typeof conf === 'object' && conf.data) {
|
|
if (typeof conf.data[0] === 'number' || RGraph.isNull(conf.data[0])) {
|
|
|
|
this.original_data[0] = RGraph.arrayClone(conf.data);
|
|
|
|
//} else if (typeof conf.data[0] === 'object' && !RGraph.isNull(conf.data[0])) {
|
|
} else {
|
|
|
|
for (var i=0; i<conf.data.length; ++i) {
|
|
this.original_data[i] = RGraph.arrayClone(conf.data[i]);
|
|
}
|
|
}
|
|
|
|
// Allow for the older configuration style
|
|
} else {
|
|
for (var i=1; i<arguments.length; ++i) {
|
|
|
|
if ( arguments[1]
|
|
&& typeof(arguments[1]) == 'object'
|
|
&& arguments[1][0]
|
|
&& typeof(arguments[1][0]) == 'object'
|
|
&& arguments[1][0].length) {
|
|
|
|
var tmp = [];
|
|
|
|
for (var i=0; i<arguments[1].length; ++i) {
|
|
tmp[i] = RGraph.array_clone(arguments[1][i]);
|
|
}
|
|
|
|
for (var j=0; j<tmp.length; ++j) {
|
|
this.original_data[j] = RGraph.array_clone(tmp[j]);
|
|
}
|
|
|
|
} else {
|
|
this.original_data[i - 1] = RGraph.array_clone(arguments[i]);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// Check for support
|
|
if (!this.canvas) {
|
|
alert('[LINE] Fatal error: no canvas support');
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* Store the data here as one big array
|
|
*/
|
|
this.data_arr = RGraph.array_linearize(this.original_data);
|
|
|
|
for (var i=0; i<this.data_arr.length; ++i) {
|
|
this['$' + i] = {};
|
|
}
|
|
|
|
|
|
/**
|
|
* Translate half a pixel for antialiasing purposes - but only if it hasn't beeen
|
|
* done already
|
|
*/
|
|
if (!this.canvas.__rgraph_aa_translated__) {
|
|
this.context.translate(0.5,0.5);
|
|
|
|
this.canvas.__rgraph_aa_translated__ = true;
|
|
}
|
|
|
|
|
|
|
|
|
|
// Short variable names
|
|
var RG = RGraph,
|
|
ca = this.canvas,
|
|
co = ca.getContext('2d'),
|
|
prop = this.properties,
|
|
pa = RG.Path,
|
|
pa2 = RG.path2,
|
|
win = window,
|
|
doc = document,
|
|
ma = Math
|
|
|
|
|
|
|
|
/**
|
|
* "Decorate" the object with the generic effects if the effects library has been included
|
|
*/
|
|
if (RG.Effects && typeof RG.Effects.decorate === 'function') {
|
|
RG.Effects.decorate(this);
|
|
}
|
|
|
|
//
|
|
// Wrap the canvas with a DIV so that DOM text can be positioned
|
|
// accurately
|
|
//
|
|
//RG.wrap(ca);
|
|
|
|
|
|
|
|
/**
|
|
* An all encompassing accessor
|
|
*
|
|
* @param string name The name of the property
|
|
* @param mixed value The value of the property
|
|
*/
|
|
this.set =
|
|
this.Set = function (name)
|
|
{
|
|
var value = typeof arguments[1] === 'undefined' ? null : arguments[1];
|
|
|
|
/**
|
|
* the number of arguments is only one and it's an
|
|
* object - parse it for configuration data and return.
|
|
*/
|
|
if (arguments.length === 1 && typeof name === 'object') {
|
|
RG.parseObjectStyleConfig(this, name);
|
|
return this;
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
* This should be done first - prepend the propertyy name with "chart." if necessary
|
|
*/
|
|
if (name.substr(0,6) != 'chart.') {
|
|
name = 'chart.' + name;
|
|
}
|
|
|
|
|
|
|
|
|
|
// Convert uppercase letters to dot+lower case letter
|
|
name = name.replace(/([A-Z])/g, function (str)
|
|
{
|
|
return '.' + String(RegExp.$1).toLowerCase();
|
|
});
|
|
|
|
|
|
|
|
// Consolidate the tooltips
|
|
if (name == 'chart.tooltips' && typeof value == 'object' && value) {
|
|
|
|
var tooltips = [];
|
|
|
|
for (var i=1; i<arguments.length; i++) {
|
|
if (typeof(arguments[i]) == 'object' && arguments[i][0]) {
|
|
for (var j=0; j<arguments[i].length; j++) {
|
|
tooltips.push(arguments[i][j]);
|
|
}
|
|
|
|
} else if (typeof(arguments[i]) == 'function') {
|
|
tooltips = arguments[i];
|
|
|
|
} else {
|
|
tooltips.push(arguments[i]);
|
|
}
|
|
}
|
|
|
|
// Because "value" is used further down at the end of this function, set it to the expanded array os tooltips
|
|
value = tooltips;
|
|
}
|
|
|
|
|
|
/**
|
|
* If (buggy) Chrome and the linewidth is 1, change it to 1.01
|
|
*/
|
|
if (name == 'chart.linewidth' && navigator.userAgent.match(/Chrome/)) {
|
|
if (value == 1) {
|
|
value = 1.01;
|
|
|
|
} else if (RGraph.is_array(value)) {
|
|
for (var i=0; i<value.length; ++i) {
|
|
if (typeof(value[i]) == 'number' && value[i] == 1) {
|
|
value[i] = 1.01;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Check for xaxispos
|
|
*/
|
|
if (name == 'chart.xaxispos' ) {
|
|
if (value != 'bottom' && value != 'center' && value != 'top') {
|
|
alert('[LINE] (' + this.id + ') chart.xaxispos should be top, center or bottom. Tried to set it to: ' + value + ' Changing it to center');
|
|
value = 'center';
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* chart.xticks is now called chart.numxticks
|
|
*/
|
|
if (name == 'chart.xticks') {
|
|
name = 'chart.numxticks';
|
|
}
|
|
|
|
|
|
/**
|
|
* Change the new chart.spline option to chart.curvy
|
|
*/
|
|
if (name == 'chart.spline') {
|
|
name = 'chart.curvy';
|
|
}
|
|
|
|
|
|
/**
|
|
* Chnge chart.ylabels.invert to chart.scale.invert
|
|
*/
|
|
if (name == 'chart.ylabels.invert') {
|
|
name = 'chart.scale.invert';
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.properties[name] = value;
|
|
|
|
return this;
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
* An all encompassing accessor
|
|
*
|
|
* @param string name The name of the property
|
|
*/
|
|
this.get =
|
|
this.Get = function (name)
|
|
{
|
|
/**
|
|
* This should be done first - prepend the property name with "chart." if necessary
|
|
*/
|
|
if (name.substr(0,6) != 'chart.') {
|
|
name = 'chart.' + name;
|
|
}
|
|
|
|
// Convert uppercase letters to dot+lower case letter
|
|
name = name.replace(/([A-Z])/g, function (str)
|
|
{
|
|
return '.' + String(RegExp.$1).toLowerCase()
|
|
});
|
|
|
|
/**
|
|
* If requested property is chart.spline - change it to chart.curvy
|
|
*/
|
|
if (name == 'chart.spline') {
|
|
name = 'chart.curvy';
|
|
}
|
|
|
|
return prop[name];
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
* The function you call to draw the line chart
|
|
*
|
|
* @param bool An optional bool used internally to ditinguish whether the
|
|
* line chart is being called by the bar chart
|
|
*
|
|
* Draw()
|
|
* |
|
|
* +--Draw()
|
|
* | |
|
|
* | +-DrawLine()
|
|
* |
|
|
* +-RedrawLine()
|
|
* |
|
|
* +-DrawCurvyLine()
|
|
* |
|
|
* +-DrawSpline()
|
|
*/
|
|
this.draw =
|
|
this.Draw = function ()
|
|
{
|
|
// MUST be the first thing done!
|
|
if (typeof(prop['chart.background.image']) == 'string') {
|
|
RG.DrawBackgroundImage(this);
|
|
}
|
|
|
|
|
|
/**
|
|
* Fire the onbeforedraw event
|
|
*/
|
|
RG.FireCustomEvent(this, 'onbeforedraw');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
* Parse the colors. This allows for simple gradient syntax
|
|
*/
|
|
if (!this.colorsParsed) {
|
|
|
|
this.parseColors();
|
|
|
|
// Don't want to do this again
|
|
this.colorsParsed = true;
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* This is new in May 2011 and facilitates indiviual gutter settings,
|
|
* eg chart.gutter.left
|
|
*/
|
|
this.gutterLeft = prop['chart.gutter.left'];
|
|
this.gutterRight = prop['chart.gutter.right'];
|
|
this.gutterTop = prop['chart.gutter.top'];
|
|
this.gutterBottom = prop['chart.gutter.bottom'];
|
|
|
|
|
|
/**
|
|
* Check for Chrome 6 and shadow
|
|
*
|
|
* TODO Remove once it's been fixed (for a while)
|
|
* 07/03/2014 - Removed
|
|
* 29/10/2011 - Looks like it's been fixed as long the linewidth is at least 1.01
|
|
* SEARCH TAGS: CHROME FIX SHADOW BUG
|
|
*/
|
|
//if ( prop['chart.shadow']
|
|
// && RG.ISCHROME
|
|
// && prop['chart.linewidth'] <= 1
|
|
// && prop['chart.chromefix']
|
|
// && prop['chart.shadow.blur'] > 0) {
|
|
// alert('[RGRAPH WARNING] Chrome has a shadow bug, meaning you should increase the linewidth to at least 1.01');
|
|
//}
|
|
|
|
|
|
// Reset the data back to that which was initially supplied
|
|
this.data = RG.array_clone(this.original_data);
|
|
|
|
|
|
// Reset the max value
|
|
this.max = 0;
|
|
|
|
/**
|
|
* Reverse the datasets so that the data and the labels tally
|
|
* COMMENTED OUT 15TH AUGUST 2011
|
|
*/
|
|
//this.data = RG.array_reverse(this.data);
|
|
|
|
if (prop['chart.filled'] && !prop['chart.filled.range'] && this.data.length > 1 && prop['chart.filled.accumulative']) {
|
|
|
|
var accumulation = [];
|
|
|
|
for (var set=0; set<this.data.length; ++set) {
|
|
for (var point=0; point<this.data[set].length; ++point) {
|
|
this.data[set][point] = Number(accumulation[point] ? accumulation[point] : 0) + this.data[set][point];
|
|
accumulation[point] = this.data[set][point];
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the maximum Y scale value
|
|
*/
|
|
if (prop['chart.ymax']) {
|
|
|
|
this.max = prop['chart.ymax'];
|
|
this.min = prop['chart.ymin'] ? prop['chart.ymin'] : 0;
|
|
|
|
this.scale2 = RG.getScale2(this, {
|
|
'max':this.max,
|
|
'min':prop['chart.ymin'],
|
|
'strict':true,
|
|
'scale.thousand':prop['chart.scale.thousand'],
|
|
'scale.point':prop['chart.scale.point'],
|
|
'scale.decimals':prop['chart.scale.decimals'],
|
|
'ylabels.count':prop['chart.ylabels.count'],
|
|
'scale.round':prop['chart.scale.round'],
|
|
'units.pre': prop['chart.units.pre'],
|
|
'units.post': prop['chart.units.post']
|
|
});
|
|
|
|
this.max = this.scale2.max ? this.scale2.max : 0;
|
|
|
|
// Check for negative values
|
|
if (!prop['chart.outofbounds']) {
|
|
for (dataset=0; dataset<this.data.length; ++dataset) {
|
|
if (RGraph.isArray(this.data[dataset])) {
|
|
for (var datapoint=0; datapoint<this.data[dataset].length; datapoint++) {
|
|
// Check for negative values
|
|
this.hasnegativevalues = (this.data[dataset][datapoint] < 0) || this.hasnegativevalues;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
} else {
|
|
|
|
this.min = prop['chart.ymin'] ? prop['chart.ymin'] : 0;
|
|
|
|
// Work out the max Y value
|
|
for (dataset=0; dataset<this.data.length; ++dataset) {
|
|
for (var datapoint=0; datapoint<this.data[dataset].length; datapoint++) {
|
|
|
|
this.max = Math.max(this.max, this.data[dataset][datapoint] ? Math.abs(parseFloat(this.data[dataset][datapoint])) : 0);
|
|
|
|
// Check for negative values
|
|
if (!prop['chart.outofbounds']) {
|
|
this.hasnegativevalues = (this.data[dataset][datapoint] < 0) || this.hasnegativevalues;
|
|
}
|
|
}
|
|
}
|
|
|
|
this.scale2 = RG.getScale2(this, {
|
|
'max':this.max,
|
|
'min':prop['chart.ymin'],
|
|
'scale.thousand':prop['chart.scale.thousand'],
|
|
'scale.point':prop['chart.scale.point'],
|
|
'scale.decimals':prop['chart.scale.decimals'],
|
|
'ylabels.count':prop['chart.ylabels.count'],
|
|
'scale.round':prop['chart.scale.round'],
|
|
'units.pre': prop['chart.units.pre'],
|
|
'units.post': prop['chart.units.post']
|
|
});
|
|
|
|
this.max = this.scale2.max ? this.scale2.max : 0;
|
|
}
|
|
|
|
/**
|
|
* Setup the context menu if required
|
|
*/
|
|
if (prop['chart.contextmenu']) {
|
|
RG.ShowContext(this);
|
|
}
|
|
|
|
/**
|
|
* Reset the coords arrays otherwise it will keep growing
|
|
*/
|
|
this.coords = [];
|
|
this.coordsText = [];
|
|
|
|
/**
|
|
* Work out a few things. They need to be here because they depend on things you can change before you
|
|
* call Draw() but after you instantiate the object
|
|
*/
|
|
this.grapharea = ca.height - this.gutterTop - this.gutterBottom;
|
|
this.halfgrapharea = this.grapharea / 2;
|
|
this.halfTextHeight = prop['chart.text.size'] / 2;
|
|
|
|
// Check the combination of the X axis position and if there any negative values
|
|
//
|
|
// 19th Dec 2010 - removed for Opera since it can be reported incorrectly whn there
|
|
// are multiple graphs on the page
|
|
if (prop['chart.xaxispos'] == 'bottom' && this.hasnegativevalues && !RG.ISOPERA) {
|
|
alert('[LINE] You have negative values and the X axis is at the bottom. This is not good...');
|
|
}
|
|
|
|
if (prop['chart.variant'] == '3d') {
|
|
RG.Draw3DAxes(this);
|
|
}
|
|
|
|
// Progressively Draw the chart
|
|
RG.background.Draw(this);
|
|
|
|
|
|
/**
|
|
* Draw any horizontal bars that have been defined
|
|
*/
|
|
if (prop['chart.background.hbars'] && prop['chart.background.hbars'].length > 0) {
|
|
RG.DrawBars(this);
|
|
}
|
|
|
|
if (prop['chart.axesontop'] == false) {
|
|
this.DrawAxes();
|
|
}
|
|
|
|
//if (typeof(shadowColor) == 'object') {
|
|
// shadowColor = RG.array_reverse(RG.array_clone(prop['chart.shadow.color']]);
|
|
//}
|
|
|
|
/**
|
|
* This facilitates the new Trace2 effect
|
|
*/
|
|
|
|
co.save()
|
|
co.beginPath();
|
|
co.rect(0, 0, ca.width * prop['chart.animation.trace.clip'], ca.height);
|
|
co.clip();
|
|
|
|
for (var i=0, j=0, len=this.data.length; i<len; i++, j++) {
|
|
|
|
co.beginPath();
|
|
|
|
/**
|
|
* Turn on the shadow if required
|
|
*/
|
|
if (!prop['chart.filled']) {
|
|
this.SetShadow(i);
|
|
}
|
|
|
|
/**
|
|
* Draw the line
|
|
*/
|
|
|
|
if (prop['chart.fillstyle']) {
|
|
if (typeof(prop['chart.fillstyle']) == 'object' && prop['chart.fillstyle'][j]) {
|
|
var fill = prop['chart.fillstyle'][j];
|
|
|
|
} else if (typeof(prop['chart.fillstyle']) == 'object' && prop['chart.fillstyle'].toString().indexOf('Gradient') > 0) {
|
|
var fill = prop['chart.fillstyle'];
|
|
|
|
} else if (typeof(prop['chart.fillstyle']) == 'string') {
|
|
var fill = prop['chart.fillstyle'];
|
|
|
|
}
|
|
} else if (prop['chart.filled']) {
|
|
var fill = prop['chart.colors'][j];
|
|
|
|
} else {
|
|
var fill = null;
|
|
}
|
|
|
|
/**
|
|
* Figure out the tickmark to use
|
|
*/
|
|
if (prop['chart.tickmarks'] && typeof(prop['chart.tickmarks']) == 'object') {
|
|
var tickmarks = prop['chart.tickmarks'][i];
|
|
} else if (prop['chart.tickmarks'] && typeof(prop['chart.tickmarks']) == 'string') {
|
|
var tickmarks = prop['chart.tickmarks'];
|
|
} else if (prop['chart.tickmarks'] && typeof(prop['chart.tickmarks']) == 'function') {
|
|
var tickmarks = prop['chart.tickmarks'];
|
|
} else {
|
|
var tickmarks = null;
|
|
}
|
|
|
|
|
|
this.DrawLine(this.data[i],
|
|
prop['chart.colors'][j],
|
|
fill,
|
|
this.GetLineWidth(j),
|
|
tickmarks,
|
|
i);
|
|
|
|
co.stroke();
|
|
}
|
|
|
|
/**
|
|
* If the line is filled re-stroke the lines
|
|
*/
|
|
if (prop['chart.filled'] && prop['chart.filled.accumulative'] && !prop['chart.curvy']) {
|
|
|
|
for (var i=0; i<this.coords2.length; ++i) {
|
|
|
|
co.beginPath();
|
|
co.lineWidth = this.GetLineWidth(i);
|
|
co.strokeStyle = prop['chart.colors'][i];
|
|
|
|
for (var j=0,len=this.coords2[i].length; j<len; ++j) {
|
|
|
|
if (j == 0 || this.coords2[i][j][1] == null || (this.coords2[i][j - 1] && this.coords2[i][j - 1][1] == null)) {
|
|
co.moveTo(this.coords2[i][j][0], this.coords2[i][j][1]);
|
|
} else {
|
|
if (prop['chart.stepped']) {
|
|
co.lineTo(this.coords2[i][j][0], this.coords2[i][j - 1][1]);
|
|
}
|
|
co.lineTo(this.coords2[i][j][0], this.coords2[i][j][1]);
|
|
}
|
|
}
|
|
|
|
co.stroke();
|
|
// No fill!
|
|
}
|
|
|
|
//Redraw the tickmarks
|
|
if (prop['chart.tickmarks']) {
|
|
|
|
co.beginPath();
|
|
|
|
co.fillStyle = 'white';
|
|
|
|
for (var i=0,len=this.coords2.length; i<len; ++i) {
|
|
|
|
co.beginPath();
|
|
co.strokeStyle = prop['chart.colors'][i];
|
|
|
|
for (var j=0; j<this.coords2[i].length; ++j) {
|
|
if (typeof(this.coords2[i][j]) == 'object' && typeof(this.coords2[i][j][0]) == 'number' && typeof(this.coords2[i][j][1]) == 'number') {
|
|
|
|
var tickmarks = typeof(prop['chart.tickmarks']) == 'object' ? prop['chart.tickmarks'][i] : prop['chart.tickmarks'];
|
|
|
|
this.DrawTick( this.coords2[i],
|
|
this.coords2[i][j][0],
|
|
this.coords2[i][j][1],
|
|
co.strokeStyle,
|
|
false,
|
|
j == 0 ? 0 : this.coords2[i][j - 1][0],
|
|
j == 0 ? 0 : this.coords2[i][j - 1][1],
|
|
tickmarks,
|
|
j);
|
|
}
|
|
}
|
|
}
|
|
|
|
co.stroke();
|
|
co.fill();
|
|
}
|
|
|
|
} else if (prop['chart.filled'] && prop['chart.filled.accumulative'] && prop['chart.curvy']) {
|
|
|
|
// Restroke the curvy filled accumulative lines
|
|
|
|
for (var i=0; i<this.coordsSpline.length; i+=1) {
|
|
co.beginPath();
|
|
co.strokeStyle = prop['chart.colors'][i];
|
|
co.lineWidth = this.GetLineWidth(i);
|
|
|
|
for (var j=0,len=this.coordsSpline[i].length; j<len; j+=1) {
|
|
|
|
var point = this.coordsSpline[i][j];
|
|
|
|
j == 0 ? co.moveTo(point[0], point[1]) : co.lineTo(point[0], point[1]);
|
|
}
|
|
|
|
co.stroke();
|
|
}
|
|
|
|
|
|
|
|
for (var i=0,len=this.coords2.length; i<len; i+=1) {
|
|
for (var j=0,len2=this.coords2[i].length; j<len2; ++j) {
|
|
if (typeof(this.coords2[i][j]) == 'object' && typeof(this.coords2[i][j][0]) == 'number' && typeof(this.coords2[i][j][1]) == 'number') {
|
|
|
|
var tickmarks = typeof prop['chart.tickmarks'] == 'object' && !RGraph.is_null(prop['chart.tickmarks']) ? prop['chart.tickmarks'][i] : prop['chart.tickmarks'];
|
|
co.strokeStyle = prop['chart.colors'][i];
|
|
this.DrawTick( this.coords2[i],
|
|
this.coords2[i][j][0],
|
|
this.coords2[i][j][1],
|
|
prop['chart.colors'][i],
|
|
false,
|
|
j == 0 ? 0 : this.coords2[i][j - 1][0],
|
|
j == 0 ? 0 : this.coords2[i][j - 1][1],
|
|
tickmarks,
|
|
j);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
}
|
|
co.restore();
|
|
|
|
// ???
|
|
co.beginPath();
|
|
|
|
|
|
|
|
|
|
/**
|
|
* If the axes have been requested to be on top, do that
|
|
*/
|
|
if (prop['chart.axesontop']) {
|
|
this.DrawAxes();
|
|
}
|
|
|
|
/**
|
|
* Draw the labels
|
|
*/
|
|
this.DrawLabels();
|
|
|
|
/**
|
|
* Draw the range if necessary
|
|
*/
|
|
this.DrawRange();
|
|
|
|
// Draw a key if necessary
|
|
if (prop['chart.key'] && prop['chart.key'].length && RG.DrawKey) {
|
|
RG.DrawKey(this, prop['chart.key'], prop['chart.colors']);
|
|
}
|
|
|
|
/**
|
|
* Draw " above" labels if enabled
|
|
*/
|
|
if (prop['chart.labels.above']) {
|
|
this.drawAboveLabels();
|
|
}
|
|
|
|
/**
|
|
* Draw the "in graph" labels
|
|
*/
|
|
RG.DrawInGraphLabels(this);
|
|
|
|
/**
|
|
* Redraw the lines if a filled range is on the cards
|
|
*/
|
|
if (prop['chart.filled'] && prop['chart.filled.range'] && this.data.length == 2) {
|
|
|
|
co.beginPath();
|
|
var len = this.coords.length / 2;
|
|
co.lineWidth = prop['chart.linewidth'];
|
|
co.strokeStyle = prop['chart.colors'][0];
|
|
|
|
for (var i=0; i<len; ++i) {
|
|
|
|
if (!RG.is_null(this.coords[i][1])) {
|
|
if (i == 0) {
|
|
co.moveTo(this.coords[i][0], this.coords[i][1]);
|
|
} else {
|
|
co.lineTo(this.coords[i][0], this.coords[i][1]);
|
|
}
|
|
}
|
|
}
|
|
|
|
co.stroke();
|
|
|
|
|
|
co.beginPath();
|
|
|
|
if (prop['chart.colors'][1]) {
|
|
co.strokeStyle = prop['chart.colors'][1];
|
|
}
|
|
|
|
for (var i=this.coords.length - 1; i>=len; --i) {
|
|
if (!RG.is_null(this.coords[i][1])) {
|
|
if (i == (this.coords.length - 1)) {
|
|
co.moveTo(this.coords[i][0], this.coords[i][1]);
|
|
} else {
|
|
co.lineTo(this.coords[i][0], this.coords[i][1]);
|
|
}
|
|
}
|
|
}
|
|
|
|
co.stroke();
|
|
|
|
|
|
} else if (prop['chart.filled'] && prop['chart.filled.range']) {
|
|
alert('[LINE] You must have only two sets of data for a filled range chart');
|
|
}
|
|
|
|
/**
|
|
* This function enables resizing
|
|
*/
|
|
if (prop['chart.resizable']) {
|
|
RG.AllowResizing(this);
|
|
}
|
|
|
|
|
|
/**
|
|
* This installs the event listeners
|
|
*/
|
|
RG.InstallEventListeners(this);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
* Fire the onfirstdraw event
|
|
*/
|
|
if (this.firstDraw) {
|
|
RG.fireCustomEvent(this, 'onfirstdraw');
|
|
this.firstDraw = false;
|
|
this.firstDrawFunc();
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
* Fire the RGraph ondraw event
|
|
*/
|
|
RG.FireCustomEvent(this, 'ondraw');
|
|
|
|
return this;
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
* Used in chaining. Runs a function there and then - not waiting for
|
|
* the events to fire (eg the onbeforedraw event)
|
|
*
|
|
* @param function func The function to execute
|
|
*/
|
|
this.exec = function (func)
|
|
{
|
|
func(this);
|
|
|
|
return this;
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
* Draws the axes
|
|
*/
|
|
this.drawAxes =
|
|
this.DrawAxes = function ()
|
|
{
|
|
//var RG = RGraph;
|
|
//var ca = this.canvas;
|
|
//var co = this.context;
|
|
//var prop = this.properties;
|
|
|
|
// Don't draw the axes?
|
|
if (prop['chart.noaxes']) {
|
|
return;
|
|
}
|
|
|
|
// Turn any shadow off
|
|
RG.noShadow(this);
|
|
|
|
co.lineWidth = prop['chart.axis.linewidth'] + 0.001;
|
|
co.lineCap = 'butt';
|
|
co.strokeStyle = prop['chart.axis.color'];
|
|
co.beginPath();
|
|
|
|
// Draw the X axis
|
|
if (prop['chart.noxaxis'] == false) {
|
|
if (prop['chart.xaxispos'] == 'center') {
|
|
co.moveTo(this.gutterLeft, Math.round((this.grapharea / 2) + this.gutterTop));
|
|
co.lineTo(ca.width - this.gutterRight, Math.round((this.grapharea / 2) + this.gutterTop));
|
|
} else if (prop['chart.xaxispos'] === 'top') {
|
|
co.moveTo(this.gutterLeft, this.gutterTop);
|
|
co.lineTo(ca.width - this.gutterRight, this.gutterTop);
|
|
} else {
|
|
co.moveTo(this.gutterLeft, ca.height - this.gutterBottom);
|
|
co.lineTo(ca.width - this.gutterRight, ca.height - this.gutterBottom);
|
|
}
|
|
}
|
|
|
|
// Draw the Y axis
|
|
if (prop['chart.noyaxis'] == false) {
|
|
if (prop['chart.yaxispos'] == 'left') {
|
|
co.moveTo(this.gutterLeft, this.gutterTop);
|
|
co.lineTo(this.gutterLeft, ca.height - this.gutterBottom);
|
|
} else {
|
|
co.moveTo(ca.width - this.gutterRight, this.gutterTop);
|
|
co.lineTo(ca.width - this.gutterRight, ca.height - this.gutterBottom);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Draw the X tickmarks
|
|
*/
|
|
if (prop['chart.noxaxis'] == false && prop['chart.numxticks'] > 0) {
|
|
|
|
var xTickInterval = (ca.width - this.gutterLeft - this.gutterRight) / prop['chart.numxticks'];
|
|
|
|
|
|
if (!xTickInterval || xTickInterval <= 0) {
|
|
xTickInterval = (ca.width - this.gutterLeft - this.gutterRight) / (prop['chart.labels'] && prop['chart.labels'].length ? prop['chart.labels'].length - 1 : 10);
|
|
}
|
|
|
|
for (x=this.gutterLeft + (prop['chart.yaxispos'] == 'left' ? xTickInterval : 0); x<=(ca.width - this.gutterRight + 1 ); x+=xTickInterval) {
|
|
|
|
if (prop['chart.yaxispos'] == 'right' && x >= (ca.width - this.gutterRight - 1) ) {
|
|
break;
|
|
}
|
|
|
|
// If the last tick is not desired...
|
|
if (prop['chart.noendxtick']) {
|
|
if (prop['chart.yaxispos'] == 'left' && x >= (ca.width - this.gutterRight - 1)) {
|
|
break;
|
|
} else if (prop['chart.yaxispos'] == 'right' && x == this.gutterLeft) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
var yStart = prop['chart.xaxispos'] == 'center' ? (this.gutterTop + (this.grapharea / 2)) - 3 : ca.height - this.gutterBottom;
|
|
var yEnd = prop['chart.xaxispos'] == 'center' ? yStart + 6 : ca.height - this.gutterBottom - (x % 60 == 0 ? prop['chart.largexticks'] * prop['chart.tickdirection'] : prop['chart.smallxticks'] * prop['chart.tickdirection']);
|
|
|
|
if (prop['chart.xaxispos'] == 'center') {
|
|
var yStart = Math.round((this.gutterTop + (this.grapharea / 2))) - 3;
|
|
var yEnd = yStart + 6;
|
|
|
|
} else if (prop['chart.xaxispos'] == 'bottom') {
|
|
var yStart = ca.height - this.gutterBottom;
|
|
var yEnd = ca.height - this.gutterBottom - (x % 60 == 0 ? prop['chart.largexticks'] * prop['chart.tickdirection'] : prop['chart.smallxticks'] * prop['chart.tickdirection']);
|
|
yEnd += 0;
|
|
|
|
|
|
} else if (prop['chart.xaxispos'] == 'top') {
|
|
yStart = this.gutterTop - 3;
|
|
yEnd = this.gutterTop;
|
|
}
|
|
|
|
co.moveTo(Math.round(x), yStart);
|
|
co.lineTo(Math.round(x), yEnd);
|
|
}
|
|
|
|
// Draw an extra tickmark if there is no X axis, but there IS a Y axis
|
|
} else if (prop['chart.noyaxis'] == false && prop['chart.numyticks'] > 0) {
|
|
if (!prop['chart.noendytick']) {
|
|
if (prop['chart.yaxispos'] == 'left') {
|
|
co.moveTo(this.gutterLeft, Math.round(ca.height - this.gutterBottom));
|
|
co.lineTo(this.gutterLeft - prop['chart.smallyticks'], Math.round(ca.height - this.gutterBottom));
|
|
} else {
|
|
co.moveTo(ca.width - this.gutterRight, Math.round(ca.height - this.gutterBottom));
|
|
co.lineTo(ca.width - this.gutterRight + prop['chart.smallyticks'], Math.round(ca.height - this.gutterBottom));
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Draw the Y tickmarks
|
|
*/
|
|
var numyticks = prop['chart.numyticks'];
|
|
|
|
if (prop['chart.noyaxis'] == false && numyticks > 0) {
|
|
var counter = 0;
|
|
var adjustment = 0;
|
|
|
|
if (prop['chart.yaxispos'] == 'right') {
|
|
adjustment = (ca.width - this.gutterLeft - this.gutterRight);
|
|
}
|
|
|
|
// X axis at the center
|
|
if (prop['chart.xaxispos'] == 'center') {
|
|
var interval = (this.grapharea / numyticks);
|
|
var lineto = (prop['chart.yaxispos'] == 'left' ? this.gutterLeft : ca.width - this.gutterRight + prop['chart.smallyticks']);
|
|
|
|
// Draw the upper halves Y tick marks
|
|
for (y=this.gutterTop; y<(this.grapharea / 2) + this.gutterTop; y+=interval) {
|
|
if (y < (this.grapharea / 2) + this.gutterTop) {
|
|
co.moveTo((prop['chart.yaxispos'] == 'left' ? this.gutterLeft - prop['chart.smallyticks'] : ca.width - this.gutterRight), Math.round(y));
|
|
co.lineTo(lineto, Math.round(y));
|
|
}
|
|
}
|
|
|
|
// Draw the lower halves Y tick marks
|
|
for (y=this.gutterTop + (this.halfgrapharea) + interval; y <= this.grapharea + this.gutterTop; y+=interval) {
|
|
co.moveTo((prop['chart.yaxispos'] == 'left' ? this.gutterLeft - prop['chart.smallyticks'] : ca.width - this.gutterRight), Math.round(y));
|
|
co.lineTo(lineto, Math.round(y));
|
|
}
|
|
|
|
// X axis at the top
|
|
} else if (prop['chart.xaxispos'] == 'top') {
|
|
var interval = (this.grapharea / numyticks);
|
|
var lineto = (prop['chart.yaxispos'] == 'left' ? this.gutterLeft : ca.width - this.gutterRight + prop['chart.smallyticks']);
|
|
|
|
// Draw the Y tick marks
|
|
for (y=this.gutterTop + interval; y <=this.grapharea + this.gutterTop; y+=interval) {
|
|
co.moveTo((prop['chart.yaxispos'] == 'left' ? this.gutterLeft - prop['chart.smallyticks'] : ca.width - this.gutterRight), Math.round(y));
|
|
co.lineTo(lineto, Math.round(y));
|
|
}
|
|
|
|
// If there's no X axis draw an extra tick
|
|
if (prop['chart.noxaxis'] && prop['chart.noendytick'] == false) {
|
|
co.moveTo((prop['chart.yaxispos'] == 'left' ? this.gutterLeft - prop['chart.smallyticks'] : ca.width - this.gutterRight), this.gutterTop);
|
|
co.lineTo(lineto, this.gutterTop);
|
|
}
|
|
|
|
// X axis at the bottom
|
|
} else {
|
|
|
|
var lineto = (prop['chart.yaxispos'] == 'left' ? this.gutterLeft - prop['chart.smallyticks'] : ca.width - this.gutterRight + prop['chart.smallyticks']);
|
|
|
|
for (y=this.gutterTop; y<(ca.height - this.gutterBottom) && counter < numyticks; y+=( (ca.height - this.gutterTop - this.gutterBottom) / numyticks) ) {
|
|
|
|
co.moveTo(this.gutterLeft + adjustment, Math.round(y));
|
|
co.lineTo(lineto, Math.round(y));
|
|
|
|
var counter = counter + 1;
|
|
}
|
|
}
|
|
|
|
// Draw an extra X tickmark
|
|
} else if (prop['chart.noxaxis'] == false && prop['chart.numxticks'] > 0) {
|
|
|
|
if (prop['chart.yaxispos'] == 'left') {
|
|
co.moveTo(this.gutterLeft, prop['chart.xaxispos'] == 'top' ? this.gutterTop : ca.height - this.gutterBottom);
|
|
co.lineTo(this.gutterLeft, prop['chart.xaxispos'] == 'top' ? this.gutterTop - prop['chart.smallxticks'] : ca.height - this.gutterBottom + prop['chart.smallxticks']);
|
|
} else {
|
|
co.moveTo(ca.width - this.gutterRight, ca.height - this.gutterBottom);
|
|
co.lineTo(ca.width - this.gutterRight, ca.height - this.gutterBottom + prop['chart.smallxticks']);
|
|
}
|
|
}
|
|
|
|
co.stroke();
|
|
|
|
/**
|
|
* This is here so that setting the color after this function doesn't
|
|
* change the color of the axes
|
|
*/
|
|
co.beginPath();
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
* Draw the text labels for the axes
|
|
*/
|
|
this.drawLabels =
|
|
this.DrawLabels = function ()
|
|
{
|
|
co.strokeStyle = 'black';
|
|
co.fillStyle = prop['chart.text.color'];
|
|
co.lineWidth = 1;
|
|
|
|
// Turn off any shadow
|
|
RG.NoShadow(this);
|
|
|
|
// This needs to be here
|
|
var font = prop['chart.text.font'];
|
|
var text_size = prop['chart.text.size'];
|
|
var decimals = prop['chart.scale.decimals'];
|
|
var context = co;
|
|
var canvas = ca;
|
|
var ymin = prop['chart.ymin'];
|
|
|
|
// Draw the Y axis labels
|
|
if (prop['chart.ylabels'] && prop['chart.ylabels.specific'] == null) {
|
|
|
|
var units_pre = prop['chart.units.pre'];
|
|
var units_post = prop['chart.units.post'];
|
|
var xpos = prop['chart.yaxispos'] == 'left' ? this.gutterLeft - 5 : ca.width - this.gutterRight + 5;
|
|
var align = prop['chart.yaxispos'] == 'left' ? 'right' : 'left';
|
|
var numYLabels = this.scale2.labels.length;
|
|
var bounding = false;
|
|
var bgcolor = prop['chart.ylabels.inside'] ? prop['chart.ylabels.inside.color'] : null;
|
|
|
|
|
|
/**
|
|
* If the Y labels are inside the Y axis, invert the alignment
|
|
*/
|
|
if (prop['chart.ylabels.inside'] == true && align == 'left') {
|
|
xpos -= 10;
|
|
align = 'right';
|
|
bounding = true;
|
|
|
|
|
|
} else if (prop['chart.ylabels.inside'] == true && align == 'right') {
|
|
xpos += 10;
|
|
align = 'left';
|
|
bounding = true;
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
* X axis in the center
|
|
*/
|
|
if (prop['chart.xaxispos'] == 'center') {
|
|
|
|
var half = this.grapharea / 2;
|
|
|
|
/**
|
|
* Draw the top half
|
|
*/
|
|
for (var i=0; i<this.scale2.labels.length; ++i) {
|
|
RG.Text2(this, {'font': font,
|
|
'size': text_size,
|
|
'x': xpos,
|
|
'y': this.gutterTop + half - (((i+1)/numYLabels) * half),
|
|
'valign': 'center',
|
|
'halign':align,
|
|
'bounding': bounding,
|
|
'boundingFill': bgcolor,
|
|
'text': this.scale2.labels[i],
|
|
'tag': 'scale'
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Draw the bottom half
|
|
*/
|
|
for (var i=0; i<this.scale2.labels.length; ++i) {
|
|
RG.Text2(this, {'font': font,
|
|
'size': text_size,
|
|
'x': xpos,
|
|
'y': this.gutterTop + half + (((i+1)/numYLabels) * half),
|
|
'valign': 'center',
|
|
'halign':align,
|
|
'bounding': bounding,
|
|
'boundingFill': bgcolor,
|
|
'text': '-' + this.scale2.labels[i],
|
|
'tag': 'scale'
|
|
});
|
|
}
|
|
|
|
// No X axis - so draw 0
|
|
if (prop['chart.noxaxis'] == true || ymin != 0 || prop['chart.scale.zerostart']) {
|
|
RG.Text2(this,{'font':font,
|
|
'size':text_size,
|
|
'x':xpos,
|
|
'y':this.gutterTop + half,
|
|
'text':prop['chart.units.pre'] + ymin.toFixed(decimals) + prop['chart.units.post'],
|
|
'bounding':bounding,
|
|
'boundingFill':bgcolor,
|
|
'valign':'center',
|
|
'halign':align,
|
|
'tag': 'scale'
|
|
});
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* X axis at the top
|
|
*/
|
|
} else if (prop['chart.xaxispos'] == 'top') {
|
|
|
|
var half = this.grapharea / 2;
|
|
|
|
if (prop['chart.scale.invert']) {
|
|
|
|
for (var i=0; i<this.scale2.labels.length; ++i) {
|
|
|
|
RG.Text2(this, {'font': font,
|
|
'size': text_size,
|
|
'x': xpos,
|
|
'y': this.gutterTop + ((i/this.scale2.labels.length) * this.grapharea),
|
|
'valign': 'center',
|
|
'halign':align,
|
|
'bounding': bounding,
|
|
'boundingFill': bgcolor,
|
|
'text': '-' + this.scale2.labels[this.scale2.labels.length - (i+1)],
|
|
'tag': 'scale'
|
|
});
|
|
}
|
|
} else {
|
|
for (var i=0; i<this.scale2.labels.length; ++i) {
|
|
RG.Text2(this, {'font': font,
|
|
'size': text_size,
|
|
'x': xpos,
|
|
'y': this.gutterTop + (((i+1)/numYLabels) * this.grapharea),
|
|
'valign': 'center',
|
|
'halign':align,
|
|
'bounding': bounding,
|
|
'boundingFill': bgcolor,
|
|
'text': '-' + this.scale2.labels[i],
|
|
'tag': 'scale'
|
|
});
|
|
}
|
|
}
|
|
|
|
// Draw the lower limit if chart.ymin is specified
|
|
if ((prop['chart.ymin'] != 0 || prop['chart.noxaxis']) || prop['chart.scale.invert'] || prop['chart.scale.zerostart']) {
|
|
RG.Text2(this, {'font':font,
|
|
'size':text_size,
|
|
'x':xpos,
|
|
'y': prop['chart.scale.invert'] ? ca.height - this.gutterBottom : this.gutterTop,
|
|
'text': (prop['chart.ymin'] != 0 ? '-' : '') + RG.number_format(this, prop['chart.ymin'].toFixed(decimals), units_pre, units_post),
|
|
'valign':'center',
|
|
'halign': align,
|
|
'bounding':bounding,
|
|
'boundingFill':bgcolor,
|
|
'tag': 'scale'});
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
* X axis labels at the bottom
|
|
*/
|
|
} else {
|
|
|
|
if (prop['chart.scale.invert']) {
|
|
|
|
// Draw the minimum value
|
|
RG.Text2(this, {'font': font,
|
|
'size': text_size,
|
|
'x': xpos,
|
|
'y': this.gutterTop,
|
|
'valign': 'center',
|
|
'halign':align,
|
|
'bounding': bounding,
|
|
'boundingFill': bgcolor,
|
|
'text': RG.number_format(this, this.min.toFixed(prop['chart.scale.decimals']), units_pre, units_post),
|
|
'tag': 'scale'
|
|
});
|
|
|
|
for (var i=0,len=this.scale2.labels.length; i<len; ++i) {
|
|
RG.Text2(this, {'font': font,
|
|
'size': text_size,
|
|
'x': xpos,
|
|
'y': this.gutterTop + (((i+1)/this.scale2.labels.length) * this.grapharea),
|
|
'valign': 'center',
|
|
'halign':align,
|
|
'bounding': bounding,
|
|
'boundingFill': bgcolor,
|
|
'text': this.scale2.labels[i],
|
|
'tag': 'scale'
|
|
});
|
|
}
|
|
} else {
|
|
for (var i=0,len=this.scale2.labels.length; i<len; ++i) {
|
|
RG.Text2(this, {'font': font,
|
|
'size': text_size,
|
|
'x': xpos,
|
|
'y': this.gutterTop + ((i/this.scale2.labels.length) * this.grapharea),
|
|
'valign': 'center',
|
|
'halign':align,
|
|
'bounding': bounding,
|
|
'boundingFill': bgcolor,
|
|
'text': this.scale2.labels[this.scale2.labels.length - (i + 1)],
|
|
'tag': 'scale'
|
|
});
|
|
}
|
|
}
|
|
|
|
// Draw the lower limit if chart.ymin is specified
|
|
if ( (prop['chart.ymin']!= 0 && !prop['chart.scale.invert'] || prop['chart.scale.zerostart'])
|
|
|| prop['chart.noxaxis']
|
|
) {
|
|
RG.Text2(this, {'font':font,
|
|
'size':text_size,
|
|
'x':xpos,
|
|
'y':prop['chart.scale.invert'] ? this.gutterTop : ca.height - this.gutterBottom,
|
|
'text':RG.number_format(this, prop['chart.ymin'].toFixed(prop['chart.scale.decimals']), units_pre, units_post),
|
|
'valign':'center',
|
|
'halign':align,
|
|
'bounding':bounding,
|
|
'boundingFill':bgcolor,
|
|
'tag': 'scale'
|
|
});
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// No X axis - so draw 0 - but not if the X axis is in the center
|
|
if ( prop['chart.noxaxis'] == true
|
|
&& prop['chart.ymin'] == null
|
|
&& prop['chart.xaxispos'] != 'center'
|
|
&& prop['chart.noendytick'] == false
|
|
) {
|
|
|
|
RG.Text2(this, {'font':font,
|
|
'size':text_size,
|
|
'x':xpos,
|
|
'y':prop['chart.xaxispos'] == 'top' ? this.gutterTop : (ca.height - this.gutterBottom),'text': prop['chart.units.pre'] + Number(0).toFixed(prop['chart.scale.decimals']) + prop['chart.units.post'],
|
|
'valign':'center',
|
|
'halign':align,
|
|
'bounding':bounding,
|
|
'boundingFill':bgcolor,
|
|
'tag':'scale'
|
|
});
|
|
}
|
|
|
|
} else if (prop['chart.ylabels'] && typeof(prop['chart.ylabels.specific']) == 'object') {
|
|
|
|
// A few things
|
|
var gap = this.grapharea / prop['chart.ylabels.specific'].length;
|
|
var halign = prop['chart.yaxispos'] == 'left' ? 'right' : 'left';
|
|
var bounding = false;
|
|
var bgcolor = null;
|
|
var ymin = prop['chart.ymin'] != null && prop['chart.ymin'];
|
|
|
|
// Figure out the X coord based on the position of the axis
|
|
if (prop['chart.yaxispos'] == 'left') {
|
|
var x = this.gutterLeft - 5;
|
|
|
|
if (prop['chart.ylabels.inside']) {
|
|
x += 10;
|
|
halign = 'left';
|
|
bounding = true;
|
|
bgcolor = 'rgba(255,255,255,0.5)';
|
|
}
|
|
|
|
} else if (prop['chart.yaxispos'] == 'right') {
|
|
var x = ca.width - this.gutterRight + 5;
|
|
|
|
if (prop['chart.ylabels.inside']) {
|
|
x -= 10;
|
|
halign = 'right';
|
|
bounding = true;
|
|
bgcolor = 'rgba(255,255,255,0.5)';
|
|
}
|
|
}
|
|
|
|
|
|
// Draw the labels
|
|
if (prop['chart.xaxispos'] == 'center') {
|
|
|
|
// Draw the top halfs labels
|
|
for (var i=0; i<prop['chart.ylabels.specific'].length; ++i) {
|
|
|
|
var y = this.gutterTop + (this.grapharea / (((prop['chart.ylabels.specific'].length - 1)) * 2) * i);
|
|
|
|
if (ymin && ymin > 0) {
|
|
var y = ((this.grapharea / 2) / (prop['chart.ylabels.specific'].length - (ymin ? 1 : 0)) ) * i;
|
|
y += this.gutterTop;
|
|
}
|
|
|
|
RG.Text2(this, {'font':font,
|
|
'size':text_size,
|
|
'x':x,
|
|
'y':y,
|
|
'text':String(prop['chart.ylabels.specific'][i]),
|
|
'valign': 'center',
|
|
'halign':halign,
|
|
'bounding':bounding,
|
|
'boundingFill':bgcolor,
|
|
'tag': 'ylabels.specific'
|
|
});
|
|
}
|
|
|
|
// Now reverse the labels and draw the bottom half
|
|
var reversed_labels = RG.array_reverse(prop['chart.ylabels.specific']);
|
|
|
|
// Draw the bottom halfs labels
|
|
for (var i=0; i<reversed_labels.length; ++i) {
|
|
|
|
var y = (this.grapharea / 2) + this.gutterTop + ((this.grapharea / ((reversed_labels.length - 1) * 2) ) * i);
|
|
|
|
RG.Text2(this, {'font':font,
|
|
'size':text_size,
|
|
'x':x,
|
|
'y':y,
|
|
'text':i == 0 ? '' : String(reversed_labels[i]),
|
|
'valign': 'center',
|
|
'halign':halign,
|
|
'bounding':bounding,
|
|
'boundingFill':bgcolor,
|
|
'tag': 'ylabels.specific'
|
|
});
|
|
}
|
|
|
|
} else if (prop['chart.xaxispos'] == 'top') {
|
|
|
|
// Reverse the labels and draw
|
|
var reversed_labels = RG.array_reverse(prop['chart.ylabels.specific']);
|
|
|
|
// Draw the bottom halfs labels
|
|
for (var i=0; i<reversed_labels.length; ++i) {
|
|
|
|
var y = (this.grapharea / (reversed_labels.length - 1)) * i;
|
|
y = y + this.gutterTop;
|
|
|
|
RG.Text2(this, {'font':font,
|
|
'size':text_size,
|
|
'x':x,
|
|
'y':y,
|
|
'text':String(reversed_labels[i]),
|
|
'valign': 'center',
|
|
'halign':halign,
|
|
'bounding':bounding,
|
|
'boundingFill':bgcolor,
|
|
'tag': 'ylabels.specific'
|
|
});
|
|
}
|
|
|
|
} else {
|
|
for (var i=0; i<prop['chart.ylabels.specific'].length; ++i) {
|
|
var y = this.gutterTop + ((this.grapharea / (prop['chart.ylabels.specific'].length - 1)) * i);
|
|
RG.Text2(this, {'font':font,
|
|
'size':text_size,
|
|
'x':x,
|
|
'y':y,
|
|
'text':String(prop['chart.ylabels.specific'][i]),
|
|
'valign':'center',
|
|
'halign':halign,
|
|
'bounding':bounding,
|
|
'boundingFill':bgcolor,
|
|
'tag': 'ylabels.specific'
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Draw the X axis labels
|
|
if (prop['chart.labels'] && prop['chart.labels'].length > 0) {
|
|
|
|
var yOffset = 5,
|
|
bordered = false,
|
|
bgcolor = null
|
|
|
|
co.fillStyle = prop['chart.labels.color'] || prop['chart.text.color'];
|
|
|
|
/**
|
|
* Text angle
|
|
*/
|
|
var angle = 0,
|
|
valign = 'top',
|
|
halign = 'center',
|
|
bold = prop['chart.labels.bold']
|
|
|
|
if (prop['chart.xlabels.inside']) {
|
|
yOffset = -5;
|
|
bordered = true;
|
|
bgcolor = prop['chart.xlabels.inside.color'];
|
|
valign = 'bottom';
|
|
}
|
|
|
|
if (prop['chart.xaxispos'] == 'top') {
|
|
valign = 'bottom';
|
|
yOffset += 2;
|
|
}
|
|
|
|
if (typeof(prop['chart.text.angle']) == 'number' && prop['chart.text.angle'] > 0) {
|
|
angle = -1 * prop['chart.text.angle'];
|
|
valign = 'center';
|
|
halign = 'right';
|
|
yOffset = 10;
|
|
|
|
if (prop['chart.xaxispos'] == 'top') {
|
|
yOffset = 10;
|
|
}
|
|
}
|
|
|
|
var numLabels = prop['chart.labels'].length;
|
|
|
|
for (i=0; i<numLabels; ++i) {
|
|
|
|
// Changed 8th Nov 2010 to be not reliant on the coords
|
|
//if (this.properties['chart.labels'][i] && this.coords && this.coords[i] && this.coords[i][0]) {
|
|
if (prop['chart.labels'][i]) {
|
|
|
|
var labelX = ((ca.width - this.gutterLeft - this.gutterRight - (2 * prop['chart.hmargin'])) / (numLabels - 1) ) * i;
|
|
labelX += this.gutterLeft + prop['chart.hmargin'];
|
|
|
|
/**
|
|
* Account for an unrelated number of labels
|
|
*/
|
|
|
|
if (this.data.length === 0 || !this.data[0] || prop['chart.labels'].length != this.data[0].length) {
|
|
labelX = this.gutterLeft + prop['chart.hmargin'] + ((ca.width - this.gutterLeft - this.gutterRight - (2 * prop['chart.hmargin'])) * (i / (prop['chart.labels'].length - 1)));
|
|
}
|
|
|
|
// This accounts for there only being one point on the chart
|
|
if (!labelX) {
|
|
labelX = this.gutterLeft + prop['chart.hmargin'];
|
|
}
|
|
|
|
if (prop['chart.xaxispos'] == 'top' && prop['chart.text.angle'] > 0) {
|
|
halign = 'left';
|
|
}
|
|
|
|
if (prop['chart.text.angle'] != 0) {
|
|
halign = 'right';
|
|
}
|
|
|
|
RG.Text2(this, {
|
|
'font':font,
|
|
'size':text_size,
|
|
'bold': bold,
|
|
'x':labelX,
|
|
'y':(prop['chart.xaxispos'] == 'top') ? this.gutterTop - yOffset - (prop['chart.xlabels.inside'] ? -22 : 0) : (ca.height - this.gutterBottom) + yOffset,
|
|
'text':String(prop['chart.labels'][i]),
|
|
'valign':valign,
|
|
'halign':halign,
|
|
'bounding':bordered,
|
|
'boundingFill':bgcolor,
|
|
'angle':angle,
|
|
'tag': 'labels'
|
|
});
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
co.stroke();
|
|
co.fill();
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Draws the line
|
|
*/
|
|
this.drawLine =
|
|
this.DrawLine = function (lineData, color, fill, linewidth, tickmarks, index)
|
|
{
|
|
// This facilitates the Rise animation (the Y value only)
|
|
if (prop['chart.animation.unfold.y'] && prop['chart.animation.factor'] != 1) {
|
|
for (var i=0; i<lineData.length; ++i) {
|
|
lineData[i] *= prop['chart.animation.factor'];
|
|
}
|
|
}
|
|
|
|
var penUp = false;
|
|
var yPos = null;
|
|
var xPos = 0;
|
|
co.lineWidth = 1;
|
|
var lineCoords = [];
|
|
|
|
/**
|
|
* Get the previous line data
|
|
*/
|
|
if (index > 0) {
|
|
var prevLineCoords = this.coords2[index - 1];
|
|
}
|
|
|
|
|
|
// Work out the X interval
|
|
var xInterval = (ca.width - (2 * prop['chart.hmargin']) - this.gutterLeft - this.gutterRight) / (lineData.length - 1);
|
|
|
|
// Loop thru each value given, plotting the line
|
|
// (FORMERLY FIRST)
|
|
for (i=0,len=lineData.length; i<len; i+=1) {
|
|
|
|
var data_point = lineData[i];
|
|
|
|
/**
|
|
* Get the yPos for the given data point
|
|
*/
|
|
var yPos = this.getYCoord(data_point);
|
|
|
|
|
|
// Null data points, and a special case for this bug:http://dev.rgraph.net/tests/ymin.html
|
|
if ( lineData[i] == null
|
|
|| (prop['chart.xaxispos'] == 'bottom' && lineData[i] < this.min && !prop['chart.outofbounds'])
|
|
|| (prop['chart.xaxispos'] == 'center' && lineData[i] < (-1 * this.max) && !prop['chart.outofbounds'])
|
|
|| (((lineData[i] < this.min && prop['chart.xaxispos'] !== 'center') || lineData[i] > this.max) && !prop['chart.outofbounds'])) {
|
|
|
|
yPos = null;
|
|
}
|
|
|
|
// Not always very noticeable, but it does have an effect
|
|
// with thick lines
|
|
co.lineCap = 'round';
|
|
co.lineJoin = 'round';
|
|
|
|
// Plot the line if we're at least on the second iteration
|
|
if (i > 0) {
|
|
xPos = xPos + xInterval;
|
|
} else {
|
|
xPos = prop['chart.hmargin'] + this.gutterLeft;
|
|
}
|
|
|
|
if (prop['chart.animation.unfold.x']) {
|
|
xPos *= prop['chart.animation.factor'];
|
|
|
|
if (xPos < prop['chart.gutter.left']) {
|
|
xPos = prop['chart.gutter.left'];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add the coords to an array
|
|
*/
|
|
this.coords.push([xPos, yPos]);
|
|
lineCoords.push([xPos, yPos]);
|
|
}
|
|
|
|
co.stroke();
|
|
|
|
// Store the coords in another format, indexed by line number
|
|
this.coords2[index] = lineCoords;
|
|
|
|
/**
|
|
* For IE only: Draw the shadow ourselves as ExCanvas doesn't produce shadows
|
|
*/
|
|
if (RG.ISOLD && prop['chart.shadow']) {
|
|
this.DrawIEShadow(lineCoords, co.shadowColor);
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Now draw the actual line [FORMERLY SECOND]
|
|
*/
|
|
co.beginPath();
|
|
// Transparent now as of 11/19/2011
|
|
co.strokeStyle = 'rgba(0,0,0,0)';
|
|
//co.strokeStyle = fill;
|
|
if (fill) {
|
|
co.fillStyle = fill;
|
|
}
|
|
|
|
var isStepped = prop['chart.stepped'];
|
|
var isFilled = prop['chart.filled'];
|
|
|
|
if (prop['chart.xaxispos'] == 'top') {
|
|
var xAxisPos = this.gutterTop;
|
|
} else if (prop['chart.xaxispos'] == 'center') {
|
|
var xAxisPos = this.gutterTop + (this.grapharea / 2);
|
|
} else if (prop['chart.xaxispos'] == 'bottom') {
|
|
var xAxisPos = ca.height - this.gutterBottom;
|
|
}
|
|
|
|
|
|
|
|
|
|
for (var i=0,len=lineCoords.length; i<len; i+=1) {
|
|
|
|
xPos = lineCoords[i][0];
|
|
yPos = lineCoords[i][1];
|
|
var set = index;
|
|
|
|
var prevY = (lineCoords[i - 1] ? lineCoords[i - 1][1] : null);
|
|
var isLast = (i + 1) == lineCoords.length;
|
|
|
|
/**
|
|
* This nullifys values which are out-of-range
|
|
*/
|
|
if (!prop['chart.outofbounds'] && (prevY < this.gutterTop || prevY > (ca.height - this.gutterBottom) ) ) {
|
|
penUp = true;
|
|
}
|
|
|
|
if (i == 0 || penUp || !yPos || !prevY || prevY < this.gutterTop) {
|
|
|
|
if (prop['chart.filled'] && !prop['chart.filled.range']) {
|
|
|
|
if (!prop['chart.outofbounds'] || prevY === null || yPos === null) {
|
|
co.moveTo(xPos + 1, xAxisPos);
|
|
}
|
|
|
|
// This facilitates the X axis being at the top
|
|
// NOTE: Also done below
|
|
if (prop['chart.xaxispos'] == 'top') {
|
|
co.moveTo(xPos + 1, xAxisPos);
|
|
}
|
|
|
|
if (isStepped && i > 0) {
|
|
co.lineTo(xPos, lineCoords[i - 1][1]);
|
|
}
|
|
|
|
co.lineTo(xPos, yPos);
|
|
|
|
} else {
|
|
|
|
if (RG.ISOLD && yPos == null) {
|
|
// Nada
|
|
} else {
|
|
co.moveTo(xPos + 1, yPos);
|
|
}
|
|
}
|
|
|
|
if (yPos == null) {
|
|
penUp = true;
|
|
|
|
} else {
|
|
penUp = false;
|
|
}
|
|
|
|
} else {
|
|
|
|
// Draw the stepped part of stepped lines
|
|
if (isStepped) {
|
|
co.lineTo(xPos, lineCoords[i - 1][1]);
|
|
}
|
|
|
|
if ((yPos >= this.gutterTop && yPos <= (ca.height - this.gutterBottom)) || prop['chart.outofbounds'] ) {
|
|
|
|
if (isLast && prop['chart.filled'] && !prop['chart.filled.range'] && prop['chart.yaxispos'] == 'right') {
|
|
xPos -= 1;
|
|
}
|
|
|
|
|
|
// Added 8th September 2009
|
|
if (!isStepped || !isLast) {
|
|
co.lineTo(xPos, yPos);
|
|
|
|
if (isFilled && lineCoords[i+1] && lineCoords[i+1][1] == null) {
|
|
co.lineTo(xPos, xAxisPos);
|
|
}
|
|
|
|
// Added August 2010
|
|
} else if (isStepped && isLast) {
|
|
co.lineTo(xPos,yPos);
|
|
}
|
|
|
|
|
|
penUp = false;
|
|
} else {
|
|
penUp = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Draw a line to the X axis if the chart is filled
|
|
*/
|
|
if (prop['chart.filled'] && !prop['chart.filled.range'] && !prop['chart.curvy']) {
|
|
|
|
// Is this needed ??
|
|
var fillStyle = prop['chart.fillstyle'];
|
|
|
|
/**
|
|
* Draw the bottom edge of the filled bit using either the X axis or the prevlinedata,
|
|
* depending on the index of the line. The first line uses the X axis, and subsequent
|
|
* lines use the prevLineCoords array
|
|
*/
|
|
if (index > 0 && prop['chart.filled.accumulative']) {
|
|
|
|
co.lineTo(xPos, prevLineCoords ? prevLineCoords[i - 1][1] : (ca.height - this.gutterBottom - 1 + (prop['chart.xaxispos'] == 'center' ? (ca.height - this.gutterTop - this.gutterBottom) / 2 : 0)));
|
|
|
|
for (var k=(i - 1); k>=0; --k) {
|
|
co.lineTo(k == 0 ? prevLineCoords[k][0] + 1: prevLineCoords[k][0], prevLineCoords[k][1]);
|
|
}
|
|
} else {
|
|
|
|
// Draw a line down to the X axis
|
|
if (prop['chart.xaxispos'] == 'top') {
|
|
co.lineTo(xPos, prop['chart.gutter.top'] + 1);
|
|
co.lineTo(lineCoords[0][0],prop['chart.gutter.top'] + 1);
|
|
} else if (typeof(lineCoords[i - 1][1]) == 'number') {
|
|
|
|
var yPosition = prop['chart.xaxispos'] == 'center' ? ((ca.height - this.gutterTop - this.gutterBottom) / 2) + this.gutterTop : ca.height - this.gutterBottom;
|
|
|
|
co.lineTo(xPos,yPosition);
|
|
co.lineTo(lineCoords[0][0],yPosition);
|
|
}
|
|
}
|
|
|
|
co.fillStyle = fill;
|
|
|
|
co.fill();
|
|
co.beginPath();
|
|
|
|
}
|
|
|
|
/**
|
|
* FIXME this may need removing when Chrome is fixed
|
|
* SEARCH TAGS: CHROME SHADOW BUG
|
|
*/
|
|
//if (false && RGraph.ISCHROME && prop['chart.shadow'] && prop['chart.chromefix'] && prop['chart.shadow.blur'] > 0) {
|
|
//
|
|
// for (var i=lineCoords.length - 1; i>=0; --i) {
|
|
// if (
|
|
// typeof(lineCoords[i][1]) != 'number'
|
|
// || (typeof(lineCoords[i+1]) == 'object' && typeof(lineCoords[i+1][1]) != 'number')
|
|
// ) {
|
|
// co.moveTo(lineCoords[i][0],lineCoords[i][1]);
|
|
// } else {
|
|
// co.lineTo(lineCoords[i][0],lineCoords[i][1]);
|
|
// }
|
|
// }
|
|
//}
|
|
|
|
co.stroke();
|
|
|
|
|
|
if (prop['chart.backdrop']) {
|
|
this.DrawBackdrop(lineCoords, color);
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
* TODO CLIP TRACE
|
|
* By using the clip() method the Trace animation can be updated.
|
|
* NOTE: Needs to be done for the filled part as well
|
|
*/
|
|
co.save();
|
|
co.beginPath();
|
|
co.rect(0,0,ca.width * prop['chart.animation.trace.clip'],ca.height);
|
|
co.clip();
|
|
|
|
// Now redraw the lines with the correct line width
|
|
this.SetShadow(index);
|
|
this.RedrawLine(lineCoords, color, linewidth, index);
|
|
co.stroke();
|
|
RG.NoShadow(this);
|
|
|
|
|
|
|
|
|
|
// Draw the tickmarks
|
|
for (var i=0; i<lineCoords.length; ++i) {
|
|
|
|
i = Number(i);
|
|
|
|
/**
|
|
* Set the color
|
|
*/
|
|
co.strokeStyle = color;
|
|
|
|
|
|
if (isStepped && i == (lineCoords.length - 1)) {
|
|
co.beginPath();
|
|
//continue;
|
|
}
|
|
|
|
if (
|
|
(
|
|
tickmarks != 'endcircle'
|
|
&& tickmarks != 'endsquare'
|
|
&& tickmarks != 'filledendsquare'
|
|
&& tickmarks != 'endtick'
|
|
&& tickmarks != 'endtriangle'
|
|
&& tickmarks != 'arrow'
|
|
&& tickmarks != 'filledarrow'
|
|
)
|
|
|| (i == 0 && tickmarks != 'arrow' && tickmarks != 'filledarrow')
|
|
|| i == (lineCoords.length - 1)
|
|
) {
|
|
|
|
var prevX = (i <= 0 ? null : lineCoords[i - 1][0]);
|
|
var prevY = (i <= 0 ? null : lineCoords[i - 1][1]);
|
|
|
|
this.DrawTick(lineData, lineCoords[i][0], lineCoords[i][1], color, false, prevX, prevY, tickmarks, i);
|
|
|
|
// Draws tickmarks on the stepped bits of stepped charts. Takend out 14th July 2010
|
|
//
|
|
//if (this.properties['chart.stepped'] && lineCoords[i + 1] && this.properties['chart.tickmarks'] != 'endsquare' && this.properties['chart.tickmarks'] != 'endcircle' && this.properties['chart.tickmarks'] != 'endtick') {
|
|
// this.DrawTick(lineCoords[i + 1][0], lineCoords[i][1], color);
|
|
//}
|
|
}
|
|
}
|
|
|
|
co.restore();
|
|
|
|
// Draw something off canvas to skirt an annoying bug
|
|
co.beginPath();
|
|
co.arc(ca.width + 50000, ca.height + 50000, 2, 0, 6.38, 1);
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
* This functions draws a tick mark on the line
|
|
*
|
|
* @param xPos int The x position of the tickmark
|
|
* @param yPos int The y position of the tickmark
|
|
* @param color str The color of the tickmark
|
|
* @param bool Whether the tick is a shadow. If it is, it gets offset by the shadow offset
|
|
*/
|
|
this.drawTick =
|
|
this.DrawTick = function (lineData, xPos, yPos, color, isShadow, prevX, prevY, tickmarks, index)
|
|
{
|
|
// Various conditions mean no tick
|
|
if (!prop['chart.line.visible']) {
|
|
return;
|
|
} else if (RG.is_null(yPos)) {
|
|
return false;
|
|
} else if ((yPos > (ca.height - this.gutterBottom)) && !prop['chart.outofbounds']) {
|
|
return;
|
|
} else if ((yPos < this.gutterTop) && !prop['chart.outofbounds']) {
|
|
return;
|
|
}
|
|
|
|
co.beginPath();
|
|
|
|
var offset = 0;
|
|
|
|
// Reset the stroke and lineWidth back to the same as what they were when the line was drawm
|
|
// UPDATE 28th July 2011 - the line width is now set to 1
|
|
co.lineWidth = prop['chart.tickmarks.linewidth'] ? prop['chart.tickmarks.linewidth'] : prop['chart.linewidth'];
|
|
co.strokeStyle = isShadow ? prop['chart.shadow.color'] : co.strokeStyle;
|
|
co.fillStyle = isShadow ? prop['chart.shadow.color'] : co.strokeStyle;
|
|
|
|
// Cicular tick marks
|
|
if ( tickmarks == 'circle'
|
|
|| tickmarks == 'filledcircle'
|
|
|| tickmarks == 'endcircle') {
|
|
|
|
if (tickmarks == 'circle'|| tickmarks == 'filledcircle' || (tickmarks == 'endcircle' && (index == 0 || index == (lineData.length - 1)))) {
|
|
co.beginPath();
|
|
co.arc(xPos + offset, yPos + offset, prop['chart.ticksize'], 0, 360 / (180 / RG.PI), false);
|
|
|
|
if (tickmarks == 'filledcircle') {
|
|
co.fillStyle = isShadow ? prop['chart.shadow.color'] : co.strokeStyle;
|
|
} else {
|
|
co.fillStyle = isShadow ? prop['chart.shadow.color'] : 'white';
|
|
}
|
|
|
|
co.stroke();
|
|
co.fill();
|
|
}
|
|
|
|
// Halfheight "Line" style tick marks
|
|
} else if (tickmarks == 'halftick') {
|
|
co.beginPath();
|
|
co.moveTo(Math.round(xPos), yPos);
|
|
co.lineTo(Math.round(xPos), yPos + prop['chart.ticksize']);
|
|
|
|
co.stroke();
|
|
|
|
// Tick style tickmarks
|
|
} else if (tickmarks == 'tick') {
|
|
co.beginPath();
|
|
co.moveTo(Math.round(xPos), yPos - prop['chart.ticksize']);
|
|
co.lineTo(Math.round(xPos), yPos + prop['chart.ticksize']);
|
|
|
|
co.stroke();
|
|
|
|
// Endtick style tickmarks
|
|
} else if (tickmarks == 'endtick' && (index == 0 || index == (lineData.length - 1))) {
|
|
co.beginPath();
|
|
co.moveTo(Math.round(xPos), yPos - prop['chart.ticksize']);
|
|
co.lineTo(Math.round(xPos), yPos + prop['chart.ticksize']);
|
|
|
|
co.stroke();
|
|
|
|
// "Cross" style tick marks
|
|
} else if (tickmarks == 'cross') {
|
|
co.beginPath();
|
|
|
|
var ticksize = prop['chart.ticksize'];
|
|
|
|
co.moveTo(xPos - ticksize, yPos - ticksize);
|
|
co.lineTo(xPos + ticksize, yPos + ticksize);
|
|
co.moveTo(xPos + ticksize, yPos - ticksize);
|
|
co.lineTo(xPos - ticksize, yPos + ticksize);
|
|
co.stroke();
|
|
|
|
|
|
// Triangle style tick marks
|
|
} else if (tickmarks == 'triangle' || tickmarks == 'filledtriangle' || (tickmarks == 'endtriangle' && (index == 0 || index == (lineData.length - 1)))) {
|
|
co.beginPath();
|
|
|
|
if (tickmarks == 'filledtriangle') {
|
|
co.fillStyle = isShadow ? prop['chart.shadow.color'] : co.strokeStyle;
|
|
} else {
|
|
co.fillStyle = 'white';
|
|
}
|
|
|
|
co.moveTo(ma.round(xPos - prop['chart.ticksize']), yPos + prop['chart.ticksize']);
|
|
co.lineTo(ma.round(xPos), yPos - prop['chart.ticksize']);
|
|
co.lineTo(ma.round(xPos + prop['chart.ticksize']), yPos + prop['chart.ticksize']);
|
|
co.closePath();
|
|
|
|
co.stroke();
|
|
co.fill();
|
|
|
|
|
|
//
|
|
// A white bordered circle
|
|
//
|
|
} else if (tickmarks == 'borderedcircle' || tickmarks == 'dot') {
|
|
|
|
co.lineWidth = prop['chart.tickmarks.dot.linewidth'] || 0.00000001;
|
|
|
|
pa(this, [
|
|
'b',
|
|
'a',xPos, yPos, prop['chart.ticksize'], 0, 360 / (180 / RG.PI), false,
|
|
'c',
|
|
'f',prop['chart.tickmarks.dot.fill'] || color,
|
|
's',prop['chart.tickmarks.dot.stroke'] || color
|
|
]);
|
|
|
|
} else if ( tickmarks == 'square'
|
|
|| tickmarks == 'filledsquare'
|
|
|| (tickmarks == 'endsquare' && (index == 0 || index == (lineData.length - 1)))
|
|
|| (tickmarks == 'filledendsquare' && (index == 0 || index == (lineData.length - 1))) ) {
|
|
|
|
co.fillStyle = 'white';
|
|
co.strokeStyle = co.strokeStyle;
|
|
|
|
co.beginPath();
|
|
co.rect(Math.round(xPos - prop['chart.ticksize']), Math.round(yPos - prop['chart.ticksize']), prop['chart.ticksize'] * 2, prop['chart.ticksize'] * 2);
|
|
|
|
// Fillrect
|
|
if (tickmarks == 'filledsquare' || tickmarks == 'filledendsquare') {
|
|
co.fillStyle = isShadow ? prop['chart.shadow.color'] : co.strokeStyle;
|
|
co.rect(Math.round(xPos - prop['chart.ticksize']), Math.round(yPos - prop['chart.ticksize']), prop['chart.ticksize'] * 2, prop['chart.ticksize'] * 2);
|
|
|
|
} else if (tickmarks == 'square' || tickmarks == 'endsquare') {
|
|
co.fillStyle = isShadow ? prop['chart.shadow.color'] : 'white';
|
|
co.rect(Math.round((xPos - prop['chart.ticksize']) + 1), Math.round((yPos - prop['chart.ticksize']) + 1), (prop['chart.ticksize'] * 2) - 2, (prop['chart.ticksize'] * 2) - 2);
|
|
}
|
|
|
|
co.stroke();
|
|
co.fill();
|
|
|
|
/**
|
|
* FILLED arrowhead
|
|
*/
|
|
} else if (tickmarks == 'filledarrow') {
|
|
|
|
var x = Math.abs(xPos - prevX);
|
|
var y = Math.abs(yPos - prevY);
|
|
|
|
if (yPos < prevY) {
|
|
var a = Math.atan(x / y) + 1.57;
|
|
} else {
|
|
var a = Math.atan(y / x) + 3.14;
|
|
}
|
|
|
|
co.beginPath();
|
|
co.moveTo(Math.round(xPos), Math.round(yPos));
|
|
co.arc(Math.round(xPos), Math.round(yPos), 7, a - 0.5, a + 0.5, false);
|
|
co.closePath();
|
|
|
|
co.stroke();
|
|
co.fill();
|
|
|
|
/**
|
|
* Arrow head, NOT filled
|
|
*/
|
|
} else if (tickmarks == 'arrow') {
|
|
|
|
var orig_linewidth = co.lineWidth;
|
|
|
|
var x = Math.abs(xPos - prevX);
|
|
var y = Math.abs(yPos - prevY);
|
|
|
|
co.lineWidth;
|
|
|
|
if (yPos < prevY) {
|
|
var a = Math.atan(x / y) + 1.57;
|
|
} else {
|
|
var a = Math.atan(y / x) + 3.14;
|
|
}
|
|
|
|
co.beginPath();
|
|
co.moveTo(Math.round(xPos), Math.round(yPos));
|
|
co.arc(Math.round(xPos), Math.round(yPos), 7, a - 0.5 - (doc.all ? 0.1 : 0.01), a - 0.4, false);
|
|
|
|
co.moveTo(Math.round(xPos), Math.round(yPos));
|
|
co.arc(Math.round(xPos), Math.round(yPos), 7, a + 0.5 + (doc.all ? 0.1 : 0.01), a + 0.5, true);
|
|
co.stroke();
|
|
co.fill();
|
|
|
|
// Revert to original lineWidth
|
|
co.lineWidth = orig_linewidth;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
* Image based tickmark
|
|
*/
|
|
// lineData, xPos, yPos, color, isShadow, prevX, prevY, tickmarks, index
|
|
} else if (
|
|
typeof tickmarks === 'string' &&
|
|
(
|
|
tickmarks.substr(0, 6) === 'image:' ||
|
|
tickmarks.substr(0, 5) === 'data:' ||
|
|
tickmarks.substr(0, 1) === '/' ||
|
|
tickmarks.substr(0, 3) === '../' ||
|
|
tickmarks.substr(0, 7) === 'images/'
|
|
)
|
|
) {
|
|
|
|
var img = new Image();
|
|
|
|
if (tickmarks.substr(0, 6) === 'image:') {
|
|
img.src = tickmarks.substr(6);
|
|
} else {
|
|
img.src = tickmarks;
|
|
}
|
|
|
|
|
|
img.onload = function ()
|
|
{
|
|
if (prop['chart.tickmarks.image.halign'] === 'center') xPos -= (this.width / 2);
|
|
if (prop['chart.tickmarks.image.halign'] === 'right') xPos -= this.width;
|
|
|
|
if (prop['chart.tickmarks.image.valign'] === 'center') yPos -= (this.height / 2);
|
|
if (prop['chart.tickmarks.image.valign'] === 'bottom') yPos -= this.height;
|
|
|
|
xPos += prop['chart.tickmarks.image.offsetx'];
|
|
yPos += prop['chart.tickmarks.image.offsety'];
|
|
|
|
co.drawImage(this, xPos, yPos);
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
* Custom tick drawing function
|
|
*/
|
|
} else if (typeof(tickmarks) == 'function') {
|
|
tickmarks(this, lineData, lineData[index], index, xPos, yPos, color, prevX, prevY);
|
|
}
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
* Draws a filled range if necessary
|
|
*/
|
|
this.drawRange =
|
|
this.DrawRange = function ()
|
|
{
|
|
//var RG = RGraph;
|
|
//var ca = this.canvas;
|
|
//var co = this.context;
|
|
//var prop = this.properties;
|
|
|
|
/**
|
|
* Fill the range if necessary
|
|
*/
|
|
if (prop['chart.filled.range'] && prop['chart.filled'] && prop['chart.line.visible']) {
|
|
|
|
if (RG.is_null(prop['chart.filled.range.threshold'])) {
|
|
prop['chart.filled.range.threshold'] = this.ymin
|
|
prop['chart.filled.range.threshold.colors'] = [prop['chart.fillstyle'], prop['chart.fillstyle']]
|
|
}
|
|
|
|
for (var idx=0; idx<2; ++idx) {
|
|
|
|
var threshold_colors = prop['chart.filled.range.threshold.colors'];
|
|
var y = this.getYCoord(prop['chart.filled.range.threshold'])
|
|
|
|
co.save();
|
|
if (idx == 0) {
|
|
co.beginPath();
|
|
co.rect(0,0,ca.width,y);
|
|
co.clip();
|
|
|
|
} else {
|
|
|
|
co.beginPath();
|
|
co.rect(0,y,ca.width, ca.height);
|
|
co.clip();
|
|
}
|
|
|
|
co.beginPath();
|
|
co.fillStyle = (idx == 1 ? prop['chart.filled.range.threshold.colors'][1] : prop['chart.filled.range.threshold.colors'][0]);
|
|
|
|
//co.strokeStyle = prop['chart.fillstyle']; // Strokestyle not used now (10th October 2012)
|
|
|
|
co.lineWidth = 1;
|
|
var len = (this.coords.length / 2);
|
|
|
|
|
|
|
|
for (var i=0; i<len; ++i) {
|
|
if (!RG.is_null(this.coords[i][1])) {
|
|
if (i == 0) {
|
|
co.moveTo(this.coords[i][0], this.coords[i][1])
|
|
} else {
|
|
co.lineTo(this.coords[i][0], this.coords[i][1])
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
for (var i=this.coords.length - 1; i>=len; --i) {
|
|
if (RG.is_null(this.coords[i][1])) {
|
|
co.moveTo(this.coords[i][0], this.coords[i][1])
|
|
} else {
|
|
co.lineTo(this.coords[i][0], this.coords[i][1])
|
|
}
|
|
//co.lineTo(this.coords[i][0], this.coords[i][1])
|
|
}
|
|
|
|
|
|
|
|
// Taken out - 10th Oct 2012
|
|
//co.stroke();
|
|
|
|
co.fill();
|
|
co.restore();
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
* Redraws the line with the correct line width etc
|
|
*
|
|
* @param array coords The coordinates of the line
|
|
*/
|
|
this.redrawLine =
|
|
this.RedrawLine = function (coords, color, linewidth, index)
|
|
{
|
|
if (prop['chart.noredraw'] || prop['chart.filled.range']) {
|
|
return;
|
|
}
|
|
|
|
co.strokeStyle = (typeof(color) == 'object' && color && color.toString().indexOf('CanvasGradient') == -1 ? color[0] : color);
|
|
co.lineWidth = linewidth;
|
|
|
|
if (!prop['chart.line.visible']) {
|
|
co.strokeStyle = 'rgba(0,0,0,0)';
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!RG.ISOLD && (prop['chart.curvy'] || prop['chart.spline'])) {
|
|
this.DrawCurvyLine(coords, !prop['chart.line.visible'] ? 'rgba(0,0,0,0)' : color, linewidth, index);
|
|
return;
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
co.beginPath();
|
|
|
|
var len = coords.length;
|
|
var width = ca.width
|
|
var height = ca.height;
|
|
var penUp = false;
|
|
|
|
for (var i=0; i<len; ++i) {
|
|
|
|
var xPos = coords[i][0];
|
|
var yPos = coords[i][1];
|
|
|
|
if (i > 0) {
|
|
var prevX = coords[i - 1][0];
|
|
var prevY = coords[i - 1][1];
|
|
}
|
|
|
|
|
|
if ((
|
|
(i == 0 && coords[i])
|
|
|| (yPos < this.gutterTop)
|
|
|| (prevY < this.gutterTop)
|
|
|| (yPos > (height - this.gutterBottom))
|
|
|| (i > 0 && prevX > (width - this.gutterRight))
|
|
|| (i > 0 && prevY > (height - this.gutterBottom))
|
|
|| prevY == null
|
|
|| penUp == true
|
|
) && (!prop['chart.outofbounds'] || yPos == null || prevY == null) ) {
|
|
|
|
if (RG.ISOLD && yPos == null) {
|
|
// ...?
|
|
} else {
|
|
co.moveTo(coords[i][0], coords[i][1]);
|
|
}
|
|
|
|
penUp = false;
|
|
|
|
} else {
|
|
|
|
if (prop['chart.stepped'] && i > 0) {
|
|
co.lineTo(coords[i][0], coords[i - 1][1]);
|
|
}
|
|
|
|
// Don't draw the last bit of a stepped chart. Now DO
|
|
//if (!this.properties['chart.stepped'] || i < (coords.length - 1)) {
|
|
co.lineTo(coords[i][0], coords[i][1]);
|
|
//}
|
|
penUp = false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* If two colors are specified instead of one, go over the up bits
|
|
*/
|
|
if (prop['chart.colors.alternate'] && typeof(color) == 'object' && color[0] && color[1]) {
|
|
for (var i=1; i<len; ++i) {
|
|
|
|
var prevX = coords[i - 1][0];
|
|
var prevY = coords[i - 1][1];
|
|
|
|
if (prevY != null && coords[i][1] != null) {
|
|
co.beginPath();
|
|
co.strokeStyle = color[coords[i][1] < prevY ? 0 : 1];
|
|
co.lineWidth = prop['chart.linewidth'];
|
|
co.moveTo(prevX, prevY);
|
|
co.lineTo(coords[i][0], coords[i][1]);
|
|
co.stroke();
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
* This function is used by MSIE only to manually draw the shadow
|
|
*
|
|
* @param array coords The coords for the line
|
|
*/
|
|
this.drawIEShadow =
|
|
this.DrawIEShadow = function (coords, color)
|
|
{
|
|
var offsetx = prop['chart.shadow.offsetx'];
|
|
var offsety = prop['chart.shadow.offsety'];
|
|
|
|
co.lineWidth = prop['chart.linewidth'];
|
|
co.strokeStyle = color;
|
|
|
|
co.beginPath();
|
|
for (var i=0; i<coords.length; ++i) {
|
|
|
|
var isNull = RG.isNull(coords[i][1]);
|
|
var prevIsNull = RG.isNull(coords[i-1]) || RG.isNull(coords[i-1][1]);
|
|
|
|
if (i == 0 || isNull || prevIsNull) {
|
|
if (!isNull) {
|
|
co.moveTo(coords[i][0] + offsetx, coords[i][1] + offsety);
|
|
}
|
|
} else {
|
|
co.lineTo(coords[i][0] + offsetx, coords[i][1] + offsety);
|
|
}
|
|
}
|
|
co.stroke();
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
* Draw the backdrop
|
|
*/
|
|
this.drawBackdrop =
|
|
this.DrawBackdrop = function (coords, color)
|
|
{
|
|
//var ca = this.canvas;
|
|
//var co = this.context;
|
|
//var prop = this.properties;
|
|
|
|
var size = prop['chart.backdrop.size'];
|
|
co.lineWidth = size;
|
|
co.globalAlpha = prop['chart.backdrop.alpha'];
|
|
co.strokeStyle = color;
|
|
var yCoords = [];
|
|
|
|
co.beginPath();
|
|
if (prop['chart.curvy'] && !RG.ISOLD) {
|
|
|
|
// The DrawSpline function only takes the Y coords so extract them from the coords that have
|
|
// (which are X/Y pairs)
|
|
for (var i=0; i<coords.length; ++i) {
|
|
yCoords.push(coords[i][1])
|
|
}
|
|
|
|
this.DrawSpline(co, yCoords, color, null);
|
|
|
|
} else {
|
|
co.moveTo(coords[0][0], coords[0][1]);
|
|
for (var j=1; j<coords.length; ++j) {
|
|
co.lineTo(coords[j][0], coords[j][1]);
|
|
}
|
|
}
|
|
co.stroke();
|
|
|
|
// Reset the alpha value
|
|
co.globalAlpha = 1;
|
|
RG.NoShadow(this);
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
* Returns the linewidth
|
|
*/
|
|
this.getLineWidth =
|
|
this.GetLineWidth = function (i)
|
|
{
|
|
var linewidth = prop['chart.linewidth'];
|
|
|
|
if (typeof(linewidth) == 'number') {
|
|
return linewidth;
|
|
|
|
} else if (typeof(linewidth) == 'object') {
|
|
if (linewidth[i]) {
|
|
return linewidth[i];
|
|
} else {
|
|
return linewidth[0];
|
|
}
|
|
|
|
alert('[LINE] Error! chart.linewidth should be a single number or an array of one or more numbers');
|
|
}
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
* The getPoint() method - used to get the point the mouse is currently over, if any
|
|
*
|
|
* @param object e The event object
|
|
* @param object OPTIONAL You can pass in the bar object instead of the
|
|
* function getting it from the canvas
|
|
*/
|
|
this.getShape =
|
|
this.getPoint = function (e)
|
|
{
|
|
var obj = this;
|
|
var RG = RGraph;
|
|
var ca = canvas = e.target;
|
|
var co = context = this.context;
|
|
var prop = this.properties;
|
|
|
|
var mouseXY = RG.getMouseXY(e);
|
|
var mouseX = mouseXY[0];
|
|
var mouseY = mouseXY[1];
|
|
|
|
// This facilitates you being able to pass in the bar object as a parameter instead of
|
|
// the function getting it from the object
|
|
if (arguments[1]) {
|
|
obj = arguments[1];
|
|
}
|
|
|
|
for (var i=0; i<obj.coords.length; ++i) {
|
|
|
|
var x = obj.coords[i][0];
|
|
var y = obj.coords[i][1];
|
|
|
|
// Do this if the hotspot is triggered by the X coord AND the Y coord
|
|
if ( mouseX <= (x + prop['chart.tooltips.hotspot.size'])
|
|
&& mouseX >= (x - prop['chart.tooltips.hotspot.size'])
|
|
&& mouseY <= (y + prop['chart.tooltips.hotspot.size'])
|
|
&& mouseY >= (y - prop['chart.tooltips.hotspot.size'])
|
|
) {
|
|
|
|
if (RG.parseTooltipText) {
|
|
var tooltip = RG.parseTooltipText(prop['chart.tooltips'], i);
|
|
}
|
|
|
|
// Work out the dataset
|
|
var dataset = 0;
|
|
var idx = i;
|
|
while ((idx + 1) > this.data[dataset].length) {
|
|
idx -= this.data[dataset].length;
|
|
dataset++;
|
|
}
|
|
|
|
return {0:obj, 1:x, 2:y, 3:i, 'object': obj, 'x': x, 'y': y, 'index': i, 'tooltip': tooltip, 'dataset': dataset, 'index_adjusted': idx};
|
|
|
|
} else if ( prop['chart.tooltips.hotspot.xonly'] == true
|
|
&& mouseX <= (x + prop['chart.tooltips.hotspot.size'])
|
|
&& mouseX >= (x - prop['chart.tooltips.hotspot.size'])) {
|
|
|
|
var tooltip = RG.parseTooltipText(prop['chart.tooltips'], i);
|
|
|
|
return {0:obj, 1:x, 2:y, 3:i, 'object': obj, 'x': x, 'y': y, 'index': i, 'tooltip': tooltip};
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
* Draws the above line labels
|
|
*/
|
|
this.drawAboveLabels =
|
|
this.DrawAboveLabels = function ()
|
|
{
|
|
var size = prop['chart.labels.above.size'],
|
|
font = prop['chart.labels.above.font'] || prop['chart.text.font'],
|
|
units_pre = prop['chart.labels.above.units.pre'],
|
|
units_post = prop['chart.labels.above.units.post'],
|
|
decimals = prop['chart.labels.above.decimals'],
|
|
color = prop['chart.labels.above.color'] || prop['chart.text.color'],
|
|
bgcolor = prop['chart.labels.above.background'] || 'white',
|
|
border = ((
|
|
typeof prop['chart.labels.above.border'] === 'boolean'
|
|
|| typeof prop['chart.labels.above.border'] === 'number'
|
|
) ? prop['chart.labels.above.border'] : true),
|
|
offsety = prop['chart.labels.above.offsety'] + size,
|
|
specific = prop['chart.labels.above.specific'];
|
|
|
|
// Use this to 'reset' the drawing state
|
|
co.beginPath();
|
|
|
|
// Don't need to check that chart.labels.above is enabled here, it's been done already
|
|
for (var i=0, len=this.coords.length; i<len; i+=1) {
|
|
|
|
var coords = this.coords[i];
|
|
|
|
RG.text2(this, {
|
|
color:color,
|
|
'font':font,
|
|
'size':size,
|
|
'x':coords[0],
|
|
'y':coords[1] - offsety,
|
|
'text':(specific && specific[i]) ? specific[i] : (specific ? null : RG.numberFormat(this, typeof decimals === 'number' ? this.data_arr[i].toFixed(decimals) : this.data_arr[i], units_pre, units_post)),
|
|
'valign':'center',
|
|
'halign':'center',
|
|
'bounding':true,
|
|
'boundingFill':bgcolor,
|
|
'boundingStroke':border ? 'black' : 'rgba(0,0,0,0)',
|
|
'tag':'labels.above'
|
|
});
|
|
}
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
* Draw a curvy line.
|
|
*/
|
|
this.drawCurvyLine =
|
|
this.DrawCurvyLine = function (coords, color, linewidth, index)
|
|
{
|
|
if (RG.ISOLD) {
|
|
return;
|
|
}
|
|
|
|
var yCoords = [];
|
|
|
|
for (var i=0; i<coords.length; ++i) {
|
|
yCoords.push(coords[i][1]);
|
|
}
|
|
|
|
if (prop['chart.filled']) {
|
|
co.beginPath();
|
|
|
|
// First, work out the xaxispos
|
|
if (prop['chart.xaxispos'] === 'center') {
|
|
var xaxisY = ((ca.height - this.gutterTop - this.gutterBottom) / 2) + this.gutterTop;
|
|
} else {
|
|
var xaxisY = ca.height - this.gutterBottom;
|
|
}
|
|
|
|
|
|
co.moveTo(coords[0][0],xaxisY);
|
|
this.drawSpline(co, yCoords, color, index);
|
|
|
|
if (prop['chart.filled.accumulative'] && index > 0) {
|
|
for (var i=(this.coordsSpline[index - 1].length - 1); i>=0; i-=1) {
|
|
co.lineTo(this.coordsSpline[index - 1][i][0], this.coordsSpline[index - 1][i][1]);
|
|
}
|
|
} else {
|
|
co.lineTo(coords[coords.length - 1][0],xaxisY);
|
|
}
|
|
co.fill();
|
|
}
|
|
|
|
co.beginPath();
|
|
this.DrawSpline(co, yCoords, color, index);
|
|
co.stroke();
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
* When you click on the chart, this method can return the Y value at that point. It works for any point on the
|
|
* chart (that is inside the gutters) - not just points on the Line.
|
|
*
|
|
* @param object e The event object
|
|
*/
|
|
this.getValue = function (arg)
|
|
{
|
|
if (arg.length == 2) {
|
|
var mouseX = arg[0];
|
|
var mouseY = arg[1];
|
|
} else {
|
|
var mouseCoords = RG.getMouseXY(arg);
|
|
var mouseX = mouseCoords[0];
|
|
var mouseY = mouseCoords[1];
|
|
}
|
|
|
|
var obj = this;
|
|
var xaxispos = prop['chart.xaxispos'];
|
|
|
|
if (mouseY < prop['chart.gutter.top']) {
|
|
return xaxispos == 'bottom' || xaxispos == 'center' ? this.max : this.min;
|
|
} else if (mouseY > (ca.height - prop['chart.gutter.bottom'])) {
|
|
return xaxispos == 'bottom' ? this.min : this.max;
|
|
}
|
|
|
|
if (prop['chart.xaxispos'] == 'center') {
|
|
var value = (( (obj.grapharea / 2) - (mouseY - prop['chart.gutter.top'])) / obj.grapharea) * (obj.max - obj.min);
|
|
value *= 2;
|
|
value > 0 ? value += this.min : value -= this.min;
|
|
return value;
|
|
} else if (prop['chart.xaxispos'] == 'top') {
|
|
var value = ((obj.grapharea - (mouseY - prop['chart.gutter.top'])) / obj.grapharea) * (obj.max - obj.min);
|
|
value = Math.abs(obj.max - value) * -1;
|
|
return value;
|
|
} else {
|
|
var value = ((obj.grapharea - (mouseY - prop['chart.gutter.top'])) / obj.grapharea) * (obj.max - obj.min)
|
|
value += obj.min;
|
|
return value;
|
|
}
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
* Each object type has its own Highlight() function which highlights the appropriate shape
|
|
*
|
|
* @param object shape The shape to highlight
|
|
*/
|
|
this.highlight =
|
|
this.Highlight = function (shape)
|
|
{
|
|
if (prop['chart.tooltips.highlight']) {
|
|
// Add the new highlight
|
|
RG.Highlight.Point(this, shape);
|
|
}
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
* The getObjectByXY() worker method. Don't call this call:
|
|
*
|
|
* RG.ObjectRegistry.getObjectByXY(e)
|
|
*
|
|
* @param object e The event object
|
|
*/
|
|
this.getObjectByXY = function (e)
|
|
{
|
|
//var ca = this.canvas;
|
|
//var prop = this.properties;
|
|
var mouseXY = RG.getMouseXY(e);
|
|
|
|
// The 5 is so that the cursor doesn't have to be over the graphArea to trigger the hotspot
|
|
if (
|
|
(mouseXY[0] > prop['chart.gutter.left'] - 5)
|
|
&& mouseXY[0] < (ca.width - prop['chart.gutter.right'] + 5)
|
|
&& mouseXY[1] > (prop['chart.gutter.top'] - 5)
|
|
&& mouseXY[1] < (ca.height - prop['chart.gutter.bottom'] + 5)
|
|
) {
|
|
|
|
return this;
|
|
}
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
* This method handles the adjusting calculation for when the mouse is moved
|
|
*
|
|
* @param object e The event object
|
|
*/
|
|
this.adjusting_mousemove =
|
|
this.Adjusting_mousemove = function (e)
|
|
{
|
|
/**
|
|
* Handle adjusting for the Bar
|
|
*/
|
|
if (prop['chart.adjustable'] && RG.Registry.Get('chart.adjusting') && RG.Registry.Get('chart.adjusting').uid == this.uid) {
|
|
|
|
// Rounding the value to the given number of decimals make the chart step
|
|
var value = Number(this.getValue(e));//.toFixed(this.properties['chart.scale.decimals']);
|
|
var shape = RG.Registry.Get('chart.adjusting.shape');
|
|
|
|
if (shape) {
|
|
|
|
RG.Registry.Set('chart.adjusting.shape', shape);
|
|
|
|
this.original_data[shape['dataset']][shape['index_adjusted']] = Number(value);
|
|
|
|
RG.redrawCanvas(e.target);
|
|
|
|
RG.fireCustomEvent(this, 'onadjust');
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
* This function can be used when the canvas is clicked on (or similar - depending on the event)
|
|
* to retrieve the relevant Y coordinate for a particular value.
|
|
*
|
|
* @param int value The value to get the Y coordinate for
|
|
*/
|
|
this.getYCoord = function (value)
|
|
{
|
|
if (typeof(value) != 'number') {
|
|
return null;
|
|
}
|
|
|
|
var y;
|
|
var xaxispos = prop['chart.xaxispos'];
|
|
|
|
// Higher than max
|
|
// Commented out on March 7th 2013 because the tan curve was not showing correctly
|
|
//if (value > this.max) {
|
|
// value = this.max;
|
|
//}
|
|
|
|
if (xaxispos == 'top') {
|
|
|
|
// Account for negative numbers
|
|
//if (value < 0) {
|
|
// value = Math.abs(value);
|
|
//}
|
|
|
|
y = ((value - this.min) / (this.max - this.min)) * this.grapharea;
|
|
|
|
// Inverted Y labels
|
|
if (prop['chart.scale.invert']) {
|
|
y = this.grapharea - y;
|
|
}
|
|
|
|
y = y + this.gutterTop
|
|
|
|
} else if (xaxispos == 'center') {
|
|
|
|
y = ((value - this.min) / (this.max - this.min)) * (this.grapharea / 2);
|
|
y = (this.grapharea / 2) - y;
|
|
y += this.gutterTop;
|
|
|
|
} else {
|
|
|
|
if ((value < this.min || value > this.max) && prop['chart.outofbounds'] == false) {
|
|
return null;
|
|
}
|
|
|
|
y = ((value - this.min) / (this.max - this.min)) * this.grapharea;
|
|
|
|
// Inverted Y labels
|
|
if (prop['chart.scale.invert']) {
|
|
y = this.grapharea - y;
|
|
}
|
|
|
|
y = ca.height - this.gutterBottom - y;
|
|
}
|
|
|
|
return y;
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
* This function positions a tooltip when it is displayed
|
|
*
|
|
* @param obj object The chart object
|
|
* @param int x The X coordinate specified for the tooltip
|
|
* @param int y The Y coordinate specified for the tooltip
|
|
* @param objec tooltip The tooltips DIV element
|
|
*/
|
|
this.positionTooltip = function (obj, x, y, tooltip, idx)
|
|
{
|
|
//var ca = obj.canvas;
|
|
//var co = obj.context;
|
|
//var prop = obj.properties;
|
|
|
|
var coordX = obj.coords[tooltip.__index__][0];
|
|
var coordY = obj.coords[tooltip.__index__][1];
|
|
var canvasXY = RG.getCanvasXY(obj.canvas);
|
|
var gutterLeft = prop['chart.gutter.left'];
|
|
var gutterTop = prop['chart.gutter.top'];
|
|
var width = tooltip.offsetWidth;
|
|
|
|
// Set the top position
|
|
tooltip.style.left = 0;
|
|
tooltip.style.top = parseInt(tooltip.style.top) - 9 + 'px';
|
|
|
|
// By default any overflow is hidden
|
|
tooltip.style.overflow = '';
|
|
|
|
// The arrow
|
|
var img = new Image();
|
|
img.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABEAAAAFCAYAAACjKgd3AAAARUlEQVQYV2NkQAN79+797+RkhC4M5+/bd47B2dmZEVkBCgcmgcsgbAaA9GA1BCSBbhAuA/AagmwQPgMIGgIzCD0M0AMMAEFVIAa6UQgcAAAAAElFTkSuQmCC';
|
|
img.style.position = 'absolute';
|
|
img.id = '__rgraph_tooltip_pointer__';
|
|
img.style.top = (tooltip.offsetHeight - 2) + 'px';
|
|
tooltip.appendChild(img);
|
|
|
|
// Reposition the tooltip if at the edges:
|
|
|
|
// LEFT edge
|
|
if ((canvasXY[0] + coordX - (width / 2)) < 10) {
|
|
tooltip.style.left = (canvasXY[0] + coordX - (width * 0.2)) + 'px';
|
|
img.style.left = ((width * 0.2) - 8.5) + 'px';
|
|
|
|
// RIGHT edge
|
|
} else if ((canvasXY[0] + coordX + (width / 2)) > doc.body.offsetWidth) {
|
|
tooltip.style.left = canvasXY[0] + coordX - (width * 0.8) + 'px';
|
|
img.style.left = ((width * 0.8) - 8.5) + 'px';
|
|
|
|
// Default positioning - CENTERED
|
|
} else {
|
|
tooltip.style.left = (canvasXY[0] + coordX - (width * 0.5)) + 'px';
|
|
img.style.left = ((width * 0.5) - 8.5) + 'px';
|
|
}
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
* This function draws a curvy line
|
|
*
|
|
* @param object context The 2D context
|
|
* @param array coords The coordinates
|
|
*/
|
|
this.drawSpline =
|
|
this.DrawSpline = function (context, coords, color, index)
|
|
{
|
|
this.coordsSpline[index] = [];
|
|
var xCoords = [];
|
|
var gutterLeft = prop['chart.gutter.left'];
|
|
var gutterRight = prop['chart.gutter.right'];
|
|
var hmargin = prop['chart.hmargin'];
|
|
var interval = (ca.width - (gutterLeft + gutterRight) - (2 * hmargin)) / (coords.length - 1);
|
|
|
|
co.strokeStyle = color;
|
|
|
|
/**
|
|
* The drawSpline function takes an array of JUST Y coords - not X/Y coords. So the line coords need converting
|
|
* if we've been given X/Y pairs
|
|
*/
|
|
for (var i=0,len=coords.length; i<len;i+=1) {
|
|
if (typeof coords[i] == 'object' && coords[i] && coords[i].length == 2) {
|
|
coords[i] = Number(coords[i][1]);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
* Get the Points array in the format we want - first value should be null along with the lst value
|
|
*/
|
|
var P = [coords[0]];
|
|
for (var i=0; i<coords.length; ++i) {
|
|
P.push(coords[i]);
|
|
}
|
|
P.push(coords[coords.length - 1] + (coords[coords.length - 1] - coords[coords.length - 2]));
|
|
|
|
for (var j=1; j<P.length-2; ++j) {
|
|
for (var t=0; t<10; ++t) {
|
|
|
|
var yCoord = Spline( t/10, P[j-1], P[j], P[j+1], P[j+2] );
|
|
|
|
xCoords.push(((j-1) * interval) + (t * (interval / 10)) + gutterLeft + hmargin);
|
|
|
|
co.lineTo(xCoords[xCoords.length - 1], yCoord);
|
|
|
|
|
|
if (typeof index == 'number') {
|
|
this.coordsSpline[index].push([xCoords[xCoords.length - 1], yCoord]);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Draw the last section
|
|
co.lineTo(((j-1) * interval) + gutterLeft + hmargin, P[j]);
|
|
if (typeof index == 'number') {
|
|
this.coordsSpline[index].push([((j-1) * interval) + gutterLeft + hmargin, P[j]]);
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function Spline (t, P0, P1, P2, P3)
|
|
{
|
|
return 0.5 * ((2 * P1) +
|
|
((0-P0) + P2) * t +
|
|
((2*P0 - (5*P1) + (4*P2) - P3) * (t*t) +
|
|
((0-P0) + (3*P1)- (3*P2) + P3) * (t*t*t)));
|
|
}
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
* This allows for easy specification of gradients
|
|
*/
|
|
this.parseColors = function ()
|
|
{
|
|
// Save the original colors so that they can be restored when the canvas is reset
|
|
if (this.original_colors.length === 0) {
|
|
this.original_colors['chart.colors'] = RGraph.array_clone(prop['chart.colors']);
|
|
this.original_colors['chart.fillstyle'] = RGraph.array_clone(prop['chart.fillstyle']);
|
|
this.original_colors['chart.key.colors'] = RGraph.array_clone(prop['chart.key.colors']);
|
|
this.original_colors['chart.background.barcolor1'] = prop['chart.background.barcolor1'];
|
|
this.original_colors['chart.background.barcolor2'] = prop['chart.background.barcolor2'];
|
|
this.original_colors['chart.background.grid.color'] = prop['chart.background.grid.color'];
|
|
this.original_colors['chart.background.color'] = prop['chart.background.color'];
|
|
this.original_colors['chart.text.color'] = prop['chart.text.color'];
|
|
this.original_colors['chart.crosshairs.color'] = prop['chart.crosshairs.color'];
|
|
this.original_colors['chart.annotate.color'] = prop['chart.annotate.color'];
|
|
this.original_colors['chart.title.color'] = prop['chart.title.color'];
|
|
this.original_colors['chart.title.yaxis.color'] = prop['chart.title.yaxis.color'];
|
|
this.original_colors['chart.key.background'] = prop['chart.key.background'];
|
|
this.original_colors['chart.axis.color'] = prop['chart.axis.color'];
|
|
this.original_colors['chart.highlight.fill'] = prop['chart.highlight.fill'];
|
|
}
|
|
|
|
|
|
|
|
for (var i=0; i<prop['chart.colors'].length; ++i) {
|
|
if (typeof(prop['chart.colors'][i]) == 'object' && prop['chart.colors'][i][0] && prop['chart.colors'][i][1]) {
|
|
prop['chart.colors'][i][0] = this.parseSingleColorForGradient(prop['chart.colors'][i][0]);
|
|
prop['chart.colors'][i][1] = this.parseSingleColorForGradient(prop['chart.colors'][i][1]);
|
|
} else {
|
|
prop['chart.colors'][i] = this.parseSingleColorForGradient(prop['chart.colors'][i]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fillstyle
|
|
*/
|
|
if (prop['chart.fillstyle']) {
|
|
if (typeof(prop['chart.fillstyle']) == 'string') {
|
|
prop['chart.fillstyle'] = this.parseSingleColorForGradient(prop['chart.fillstyle'], 'vertical');
|
|
} else {
|
|
for (var i=0; i<prop['chart.fillstyle'].length; ++i) {
|
|
prop['chart.fillstyle'][i] = this.parseSingleColorForGradient(prop['chart.fillstyle'][i], 'vertical');
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Key colors
|
|
*/
|
|
if (!RG.is_null(prop['chart.key.colors'])) {
|
|
for (var i=0; i<prop['chart.key.colors'].length; ++i) {
|
|
prop['chart.key.colors'][i] = this.parseSingleColorForGradient(prop['chart.key.colors'][i]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parse various properties for colors
|
|
*/
|
|
var properties = [
|
|
'chart.background.barcolor1',
|
|
'chart.background.barcolor2',
|
|
'chart.background.grid.color',
|
|
'chart.background.color',
|
|
'chart.text.color',
|
|
'chart.crosshairs.color',
|
|
'chart.annotate.color',
|
|
'chart.title.color',
|
|
'chart.title.yaxis.color',
|
|
'chart.key.background',
|
|
'chart.axis.color',
|
|
'chart.highlight.fill'
|
|
];
|
|
|
|
for (var i=0; i<properties.length; ++i) {
|
|
prop[properties[i]] = this.parseSingleColorForGradient(prop[properties[i]]);
|
|
}
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
* Use this function to reset the object to the post-constructor state. Eg reset colors if
|
|
* need be etc
|
|
*/
|
|
this.reset = function ()
|
|
{
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
* This parses a single color value
|
|
*/
|
|
this.parseSingleColorForGradient = function (color)
|
|
{
|
|
if (!color || typeof(color) != 'string') {
|
|
return color;
|
|
}
|
|
|
|
/**
|
|
* Horizontal or vertical gradient
|
|
*/
|
|
var dir = typeof(arguments[1]) == 'string' ? arguments[1] : 'vertical';
|
|
|
|
if (typeof color === 'string' && color.match(/^gradient\((.*)\)$/i)) {
|
|
|
|
var parts = RegExp.$1.split(':');
|
|
|
|
// Create the gradient
|
|
if (dir == 'horizontal') {
|
|
var grad = co.createLinearGradient(0,0,ca.width,0);
|
|
} else {
|
|
var grad = co.createLinearGradient(0,ca.height - prop['chart.gutter.bottom'],0,prop['chart.gutter.top']);
|
|
}
|
|
|
|
var diff = 1 / (parts.length - 1);
|
|
|
|
grad.addColorStop(0, RG.trim(parts[0]));
|
|
|
|
for (var j=1; j<parts.length; ++j) {
|
|
grad.addColorStop(j * diff, RG.trim(parts[j]));
|
|
}
|
|
}
|
|
|
|
return grad ? grad : color;
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
* Sets the appropriate shadow
|
|
*/
|
|
this.setShadow =
|
|
this.SetShadow = function (i)
|
|
{
|
|
//var ca = this.canvas;
|
|
//var co = this.context;
|
|
//var prop = this.properties;
|
|
|
|
if (prop['chart.shadow']) {
|
|
/**
|
|
* Handle the appropriate shadow color. This now facilitates an array of differing
|
|
* shadow colors
|
|
*/
|
|
var shadowColor = prop['chart.shadow.color'];
|
|
|
|
/**
|
|
* Accommodate an array of shadow colors as well as a single string
|
|
*/
|
|
if (typeof(shadowColor) == 'object' && shadowColor[i - 1]) {
|
|
co.shadowColor = shadowColor[i];
|
|
|
|
} else if (typeof(shadowColor) == 'object') {
|
|
co.shadowColor = shadowColor[0];
|
|
|
|
} else if (typeof(shadowColor) == 'string') {
|
|
co.shadowColor = shadowColor;
|
|
}
|
|
|
|
co.shadowBlur = prop['chart.shadow.blur'];
|
|
co.shadowOffsetX = prop['chart.shadow.offsetx'];
|
|
co.shadowOffsetY = prop['chart.shadow.offsety'];
|
|
}
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
* This function handles highlighting an entire data-series for the interactive
|
|
* key
|
|
*
|
|
* @param int index The index of the data series to be highlighted
|
|
*/
|
|
this.interactiveKeyHighlight = function (index)
|
|
{
|
|
var coords = this.coords2[index];
|
|
|
|
if (coords) {
|
|
|
|
var pre_linewidth = co.lineWidth;
|
|
var pre_linecap = co.lineCap;
|
|
|
|
co.lineWidth = prop['chart.linewidth'] + 10;
|
|
co.lineCap = 'round';
|
|
co.strokeStyle = prop['chart.key.interactive.highlight.chart.stroke'];
|
|
|
|
|
|
co.beginPath();
|
|
if (prop['chart.curvy']) {
|
|
this.DrawSpline(co, coords, prop['chart.key.interactive.highlight.chart'], null);
|
|
} else {
|
|
for (var i=0,len=coords.length; i<len; i+=1) {
|
|
if ( i == 0
|
|
|| RG.is_null(coords[i][1])
|
|
|| (typeof coords[i - 1][1] != undefined && RG.is_null(coords[i - 1][1]))) {
|
|
co.moveTo(coords[i][0], coords[i][1]);
|
|
} else {
|
|
co.lineTo(coords[i][0], coords[i][1]);
|
|
}
|
|
}
|
|
}
|
|
co.stroke();
|
|
|
|
// Reset the lineCap and lineWidth
|
|
co.lineWidth = pre_linewidth;
|
|
co.lineCap = pre_linecap;
|
|
}
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
* Using a function to add events makes it easier to facilitate method chaining
|
|
*
|
|
* @param string type The type of even to add
|
|
* @param function func
|
|
*/
|
|
this.on = function (type, func)
|
|
{
|
|
if (type.substr(0,2) !== 'on') {
|
|
type = 'on' + type;
|
|
}
|
|
|
|
this[type] = func;
|
|
|
|
return this;
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
* This function runs once only
|
|
* (put at the end of the file (before any effects))
|
|
*/
|
|
this.firstDrawFunc = function ()
|
|
{
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
* Trace
|
|
*
|
|
* This effect is for the Line chart, uses the jQuery library and slowly
|
|
* uncovers the Line , but you can see the background of the chart. This effect
|
|
* is quite new (1/10/2011) and as such should be used with caution.
|
|
*
|
|
* @param object An object of configuration. You can give 'duration' or 'frames' here.
|
|
* @param function An optional callback function
|
|
*/
|
|
this.trace = function ()
|
|
{
|
|
var obj = this;
|
|
var callback = typeof arguments[1] === 'function' ? arguments[1] : function () {};
|
|
var opt = arguments[0] || {};
|
|
|
|
if (opt.frames) {
|
|
opt.duration = (opt.frames / 60) * 1000;
|
|
}
|
|
|
|
if (!opt.duration) {
|
|
opt.duration = 1500;
|
|
}
|
|
|
|
RG.clear(obj.canvas);
|
|
RG.redrawCanvas(obj.canvas);
|
|
|
|
/**
|
|
* Create the DIV that the second canvas will sit in
|
|
*/
|
|
var div = doc.createElement('DIV');
|
|
var xy = RG.getCanvasXY(obj.canvas);
|
|
div.id = '__rgraph_trace_animation_' + RG.random(0, 4351623) + '__';
|
|
div.style.left = xy[0] + 'px';
|
|
div.style.top = xy[1] + 'px';
|
|
div.style.width = obj.Get('chart.gutter.left');
|
|
div.style.height = obj.canvas.height + 'px';
|
|
div.style.position = 'absolute';
|
|
div.style.overflow = 'hidden';
|
|
doc.body.appendChild(div);
|
|
|
|
obj.canvas.__rgraph_trace_div__ = div;
|
|
|
|
/**
|
|
* Make the second canvas
|
|
*/
|
|
var id = '__rgraph_line_trace_animation_' + RG.random(0, 99999999) + '__';
|
|
var canvas2 = doc.createElement('CANVAS');
|
|
|
|
|
|
|
|
|
|
// Copy the 3D CSS transformation properties across from the original canvas
|
|
var properties = ['WebkitTransform','MozTransform','OTransform','MSTransform','transform'];
|
|
|
|
for (i in properties) {
|
|
var name = properties[i];
|
|
if (typeof obj.canvas.style[name] === 'string' && obj.canvas.style[name]) {
|
|
canvas2.style[name] = obj.canvas.style[name];
|
|
}
|
|
}
|
|
|
|
|
|
|
|
obj.canvas.__rgraph_line_canvas2__ = canvas2;
|
|
canvas2.width = obj.canvas.width;
|
|
canvas2.height = obj.canvas.height;
|
|
canvas2.style.position = 'absolute';
|
|
canvas2.style.left = 0;
|
|
canvas2.style.top = 0;
|
|
|
|
|
|
// This stops the clear effect clearing the canvas - which can happen if you have multiple canvas tags on the page all with
|
|
// dynamic effects that do redrawing
|
|
canvas2.noclear = true;
|
|
|
|
canvas2.id = id;
|
|
div.appendChild(canvas2);
|
|
|
|
var reposition_canvas2 = function (e)
|
|
{
|
|
var xy = RG.getCanvasXY(obj.canvas);
|
|
|
|
div.style.left = xy[0] + 'px';
|
|
div.style.top = xy[1] + 'px';
|
|
}
|
|
window.addEventListener('resize', reposition_canvas2, false)
|
|
|
|
/**
|
|
* Make a copy of the original Line object
|
|
*/
|
|
var obj2 = new RG.Line(id, RG.array_clone(obj.original_data));
|
|
|
|
// Remove the new line from the ObjectRegistry so that it isn't redawn
|
|
RG.ObjectRegistry.Remove(obj2);
|
|
|
|
for (i in obj.properties) {
|
|
if (typeof i === 'string') {
|
|
obj2.Set(i, obj.properties[i]);
|
|
}
|
|
}
|
|
|
|
//obj2.Set('chart.tooltips', null);
|
|
obj2.Set('labels', []);
|
|
obj2.Set('background.grid', false);
|
|
obj2.Set('background.barcolor1', 'rgba(0,0,0,0)');
|
|
obj2.Set('background.barcolor2', 'rgba(0,0,0,0)');
|
|
obj2.Set('ylabels', false);
|
|
obj2.Set('noaxes', true);
|
|
obj2.Set('title', '');
|
|
obj2.Set('title.xaxis', '');
|
|
obj2.Set('title.yaxis', '');
|
|
obj2.Set('filled.accumulative', obj.Get('chart.filled.accumulative'));
|
|
obj.Set('key', []);
|
|
obj2.Draw();
|
|
|
|
obj.canvas.__rgraph_trace_obj2__ = obj2;
|
|
|
|
|
|
/**
|
|
* This effectively hides the line
|
|
*/
|
|
obj.Set('line.visible', false);
|
|
obj.Set('colors', ['rgba(0,0,0,0)']);
|
|
if (obj.Get('filled')) {
|
|
var original_fillstyle = obj.Get('chart.fillstyle');
|
|
obj.Set('fillstyle', 'rgba(0,0,0,0)');
|
|
obj.Set('animation.trace.original.fillstyle', original_fillstyle);
|
|
}
|
|
|
|
RG.clear(obj.canvas);
|
|
//obj.Draw();
|
|
RG.redrawCanvas(obj.canvas);
|
|
|
|
/**
|
|
* Place a DIV over the canvas to stop interaction with it
|
|
*/
|
|
if (!obj.canvas.__rgraph_trace_cover__) {
|
|
var div2 = doc.createElement('DIV');
|
|
div2.id = '__rgraph_trace_animation_' + RG.random(0, 4351623) + '__';
|
|
div2.style.left = xy[0] + 'px';
|
|
div2.style.top = xy[1] + 'px';
|
|
div2.style.width = obj.canvas.width + 'px';
|
|
div2.style.height = obj.canvas.height + 'px';
|
|
div2.style.position = 'absolute';
|
|
div2.style.overflow = 'hidden';
|
|
div2.style.backgroundColor = 'rgba(0,0,0,0)';
|
|
div.div2 = div2;
|
|
obj.canvas.__rgraph_trace_cover__ = div2;
|
|
doc.body.appendChild(div2);
|
|
} else {
|
|
div2 = obj.canvas.__rgraph_trace_cover__;
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Get rid of the second canvas and turn the line back on
|
|
* on the original.
|
|
*/
|
|
trace_complete = function (obj)
|
|
{
|
|
var obj2 = obj.canvas.__rgraph_trace_obj2__;
|
|
|
|
// Remove the window resize listener
|
|
win.removeEventListener('resize', reposition_canvas2, false);
|
|
|
|
div.style.display = 'none';
|
|
div2.style.display = 'none';
|
|
|
|
//div.removeChild(canvas2);
|
|
obj.Set('line.visible', true);
|
|
|
|
// Revert the filled status back to as it was
|
|
obj.Set('filled', RGraph.array_clone(obj2.Get('chart.filled')));
|
|
obj.Set('fillstyle', obj.Get('chart.animation.trace.original.fillstyle'));
|
|
obj.Set('colors', RGraph.array_clone(obj2.Get('chart.colors')));
|
|
obj.Set('key', RGraph.array_clone(obj2.Get('chart.key')));
|
|
|
|
RGraph.RedrawCanvas(obj.canvas);
|
|
|
|
obj.canvas.__rgraph_trace_div__.style.display = 'none';
|
|
obj.canvas.__rgraph_line_canvas2__.style.display = 'none';
|
|
obj.canvas.__rgraph_trace_cover__.style.display = 'none';
|
|
obj.canvas.__rgraph_trace_div__ = null;
|
|
obj.canvas.__rgraph_line_canvas2__ = null;
|
|
obj.canvas.__rgraph_trace_cover__ = null;
|
|
|
|
|
|
callback(obj);
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
* Animate the DIV that contains the canvas
|
|
*/
|
|
jQuery('#' + div.id).animate({
|
|
width: obj.canvas.width - obj.gutterRight + 'px'
|
|
}, opt.duration, function () {trace_complete(obj)});
|
|
|
|
return this;
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
* Unfold
|
|
*
|
|
* This effect gradually increases the X/Y coordinatesfrom 0
|
|
*
|
|
* @param object obj The chart object
|
|
*/
|
|
this.unfold = function ()
|
|
{
|
|
var obj = this;
|
|
var opt = arguments[0] ? arguments[0] : {};
|
|
var frames = opt.frames ? opt.frames : 30;
|
|
var frame = 0;
|
|
var callback = arguments[1] ? arguments[1] : function () {};
|
|
var initial = prop['chart.animation.unfold.initial'];
|
|
|
|
prop['chart.animation.factor'] = prop['chart.animation.unfold.initial'];
|
|
|
|
function iterator ()
|
|
{
|
|
prop['chart.animation.factor'] = ((1 - initial) * (frame / frames)) + initial;
|
|
|
|
RG.clear(obj.canvas);
|
|
RG.redrawCanvas(obj.canvas);
|
|
|
|
if (frame < frames) {
|
|
frame++;
|
|
RG.Effects.updateCanvas(iterator);
|
|
} else {
|
|
callback(obj);
|
|
}
|
|
}
|
|
|
|
|
|
iterator();
|
|
|
|
return this;
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
* Trace2
|
|
*
|
|
* This is a new version of the Trace effect which no longer requires jQuery and is more compatible
|
|
* with other effects (eg Expand). This new effect is considerably simpler and less code.
|
|
*
|
|
* @param object Options for the effect. Currently only "frames" is available.
|
|
* @param int A function that is called when the ffect is complete
|
|
*/
|
|
this.trace2 = function ()
|
|
{
|
|
var obj = this;
|
|
var callback = arguments[2];
|
|
var opt = arguments[0] || {};
|
|
var frames = opt.frames || 30;
|
|
var frame = 0;
|
|
var callback = arguments[1] || function () {};
|
|
|
|
obj.Set('animation.trace.clip', 0);
|
|
|
|
function iterator ()
|
|
{
|
|
RG.clear(obj.canvas);
|
|
RG.redrawCanvas(obj.canvas);
|
|
|
|
if (frame++ < frames) {
|
|
obj.Set('animation.trace.clip', frame / frames);
|
|
RG.Effects.updateCanvas(iterator);
|
|
} else {
|
|
callback(obj);
|
|
}
|
|
}
|
|
|
|
iterator();
|
|
|
|
return this;
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
* FoldToCenter
|
|
*
|
|
* Line chart FoldTocenter
|
|
*
|
|
* @param object OPTIONAL An object map of options
|
|
* @param function OPTIONAL A callback to run when the effect is complete
|
|
*/
|
|
this.foldtocenter =
|
|
this.foldToCenter = function ()
|
|
{
|
|
var obj = this;
|
|
var opt = arguments[0] || {};
|
|
var frames = opt.frames || 30;
|
|
var frame = 0;
|
|
var callback = arguments[1] || function () {};
|
|
var center_value = obj.scale2.max / 2;
|
|
|
|
obj.Set('chart.ymax', obj.scale2.max);
|
|
|
|
var original_data = RG.array_clone(obj.original_data);
|
|
|
|
function iterator ()
|
|
{
|
|
for (var i=0,len=obj.data.length; i<len; ++i) {
|
|
if (obj.data[i].length) {
|
|
for (var j=0,len2=obj.data[i].length; j<len2; ++j) {
|
|
|
|
var dataset = obj.original_data[i];
|
|
|
|
if (dataset[j] > center_value) {
|
|
dataset[j] = original_data[i][j] - ((original_data[i][j] - center_value) * (frame / frames));
|
|
} else {
|
|
dataset[j] = original_data[i][j] + (((center_value - original_data[i][j]) / frames) * frame);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
RG.clear(obj.canvas);
|
|
RG.redrawCanvas(obj.canvas)
|
|
|
|
if (frame++ < frames) {
|
|
RG.Effects.updateCanvas(iterator);
|
|
} else {
|
|
callback(obj);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
iterator();
|
|
|
|
|
|
|
|
return this;
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
* UnfoldFromCenterTrace effect
|
|
*
|
|
* @param object An object containing options
|
|
* @param function A callback function
|
|
*/
|
|
this.unfoldFromCenterTrace =
|
|
this.unfoldFromCenterTrace2 = function ()
|
|
{
|
|
var obj = this;
|
|
var opt = arguments[0] || {};
|
|
var frames = opt.frames || 30;
|
|
var frame = 0;
|
|
var data = RG.array_clone(obj.original_data);
|
|
var callback = arguments[1] || function () {};
|
|
|
|
|
|
|
|
// Draw the chart once to get the scale values
|
|
obj.canvas.style.visibility = 'hidden';
|
|
obj.Draw();
|
|
var max = obj.scale2.max;
|
|
RG.clear(obj.canvas);
|
|
obj.canvas.style.visibility = 'visible';
|
|
|
|
|
|
|
|
|
|
/**
|
|
* When the Trace function finishes it calls this function
|
|
*/
|
|
var unfoldCallback = function ()
|
|
{
|
|
obj.original_data = data;
|
|
obj.unfoldFromCenter({frames: frames / 2}, callback);
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
* Determine the mid-point
|
|
*/
|
|
var half = obj.Get('chart.xaxispos') == 'center' ? obj.min : ((obj.max - obj.min) / 2) + obj.min;
|
|
obj.Set('chart.ymax', obj.max);
|
|
|
|
for (var i=0,len=obj.original_data.length; i<len; ++i) {
|
|
for (var j=0; j<obj.original_data[i].length; ++j) {
|
|
obj.original_data[i][j] = (obj.Get('chart.filled') && obj.Get('chart.filled.accumulative') && i > 0) ? 0 : half;
|
|
}
|
|
}
|
|
|
|
RG.clear(obj.canvas);
|
|
obj.trace2({frames: frames / 2}, unfoldCallback);
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
* UnfoldFromCenter
|
|
*
|
|
* Line chart unfold from center
|
|
*
|
|
* @param object An option map of properties. Only frames is supported: {frames: 30}
|
|
* @param function An optional callback
|
|
*/
|
|
this.unfoldFromCenter = function ()
|
|
{
|
|
var obj = this;
|
|
var opt = arguments[0] || {};
|
|
var frames = opt.frames || 30;
|
|
var frame = 0;
|
|
var callback = arguments[1] || function () {};
|
|
|
|
// Draw the chart once to get the scale values
|
|
obj.canvas.style.visibility = 'hidden';
|
|
obj.Draw();
|
|
var max = obj.scale2.max;
|
|
RG.clear(obj.canvas);
|
|
obj.canvas.style.visibility = 'visible';
|
|
|
|
var center_value = obj.Get('chart.xaxispos') === 'center' ? prop['chart.ymin'] : ((obj.max - obj.min) / 2) + obj.min;
|
|
var original_data = RG.array_clone(obj.original_data);
|
|
var steps = null;
|
|
|
|
obj.Set('chart.ymax', max);
|
|
|
|
if (!steps) {
|
|
|
|
steps = [];
|
|
|
|
for (var dataset=0,len=original_data.length; dataset<len; ++dataset) {
|
|
|
|
steps[dataset] = []
|
|
|
|
for (var i=0,len2=original_data[dataset].length; i<len2; ++i) {
|
|
if (prop['chart.filled'] && prop['chart.filled.accumulative'] && dataset > 0) {
|
|
steps[dataset][i] = original_data[dataset][i] / frames;
|
|
obj.original_data[dataset][i] = center_value;
|
|
} else {
|
|
steps[dataset][i] = (original_data[dataset][i] - center_value) / frames;
|
|
obj.original_data[dataset][i] = center_value;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function unfoldFromCenter ()
|
|
{
|
|
for (var dataset=0; dataset<original_data.length; ++dataset) {
|
|
for (var i=0; i<original_data[dataset].length; ++i) {
|
|
obj.original_data[dataset][i] += steps[dataset][i];
|
|
}
|
|
}
|
|
|
|
RG.clear(obj.canvas);
|
|
RG.redrawCanvas(obj.canvas);
|
|
|
|
if (--frames > 0) {
|
|
RG.Effects.updateCanvas(unfoldFromCenter);
|
|
} else {
|
|
obj.original_data = RG.array_clone(original_data);
|
|
RG.clear(obj.canvas);
|
|
RG.redrawCanvas(obj.canvas);
|
|
|
|
callback(obj);
|
|
}
|
|
}
|
|
|
|
unfoldFromCenter();
|
|
|
|
return this;
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
RG.att(ca);
|
|
|
|
|
|
|
|
|
|
/**
|
|
* Register the object so it is redrawn when necessary
|
|
*/
|
|
RG.Register(this);
|
|
|
|
|
|
|
|
|
|
/**
|
|
* This is the 'end' of the constructor so if the first argument
|
|
* contains configuration data - handle that.
|
|
*/
|
|
if (parseConfObjectForOptions) {
|
|
RG.parseObjectStyleConfig(this, conf.options);
|
|
}
|
|
}; |