function lineChart( parentElement, xTitle, yTitle, dataSets, options, xBands, yBands ) {
function plotPoint( ctx, x, y, r, pointType ) {
var sin60 = 0.866, cos60 = 0.5, tan30 = 0.57735;
pointType %= 6;
switch( pointType ) {
case 1:
r *= 0.8;
ctx.fillRect( x - r, y - r, 2 * r, 2 * r );
break;
case 2:
ctx.beginPath();
ctx.moveTo( x, y - r );
ctx.lineTo( x + r * sin60, y + r * cos60 );
ctx.lineTo( x - r * sin60, y + r * cos60 );
ctx.closePath();
ctx.fill();
break;
case 3:
ctx.beginPath();
ctx.moveTo( x, y + r );
ctx.lineTo( x + r * sin60, y - r * cos60 );
ctx.lineTo( x - r * sin60, y - r * cos60 );
ctx.closePath();
ctx.fill();
break;
case 4:
ctx.beginPath();
ctx.moveTo( x, y - r );
ctx.lineTo( x + r * cos60 * tan30, y - r * cos60 );
ctx.lineTo( x + r * sin60, y - r * cos60 );
ctx.lineTo( x + r * tan30, y );
ctx.lineTo( x + r * sin60, y + r * cos60 );
ctx.lineTo( x + r * cos60 * tan30, y + r * cos60 );
ctx.lineTo( x, y + r );
ctx.lineTo( x - r * cos60 * tan30, y + r * cos60 );
ctx.lineTo( x - r * sin60, y + r * cos60 );
ctx.lineTo( x - r * tan30, y );
ctx.lineTo( x - r * sin60, y - r * cos60 );
ctx.lineTo( x - r * cos60 * tan30, y - r * cos60 );
ctx.closePath();
ctx.fill();
break;
case 5:
r *= 0.8 * Math.SQRT2;
ctx.beginPath();
ctx.moveTo( x, y - r );
ctx.lineTo( x + r, y );
ctx.lineTo( x, y + r );
ctx.lineTo( x - r, y );
ctx.closePath();
ctx.fill();
break;
default:
ctx.beginPath();
ctx.arc( x, y, r, 0, Math.PI * 2, true );
ctx.fill();
}
}
function drawPoint( x, y, r, pointType, colour, totalPoints ) {
var offset;
if( ( window.chrome && totalPoints > 37300 ) || ( !window.chrome && totalPoints > 2 ) ) {
offset = Math.ceil(r) + 1;
if( lastPoint.r != r || lastPoint.type != pointType || lastPoint.colour != colour ) {
lastPoint.r = r;
lastPoint.type = pointType;
canvasCache.height = canvasCache.width = r * 2 + 3;
contextCache.clearRect(0, 0, r * 2 + 3, r * 2 + 3);
contextCache.fillStyle = lastPoint.colour = colour;
plotPoint( contextCache, offset, offset, r, pointType );
}
context.drawImage( canvasCache, x - offset, y - offset );
} else {
plotPoint( context, x, y, r, pointType );
}
}
function padString( num, length, padChar, right ) {
var negative = num < 0;
num = Math.abs(num) + '';
while( num.length < length ) {
num = ( right ? '' : padChar ) + num + ( right ? padChar : '' );
}
return ( negative ? '-' : '' ) + num;
}
function niceDate( date, utcOrNot, workingIn ) {
var dateString = '', timezone, year, separatorS = false, separatorM = false, separatorMo = false, showAnyway;
date = new Date(date);
if( window.Intl && Intl.DateTimeFormat ) {
if( !dateFormatter ) {
utcOrNot = utcOrNot || undefined;
if( workingIn == 'milliseconds' ) {
dateFormatter = new Intl.DateTimeFormat( usedFormat.locales, { year: usedFormat.year, month: usedFormat.month, day: usedFormat.day, hour: usedFormat.hour, minute: usedFormat.minute, second: usedFormat.second, fractionalSecondDigits: usedFormat.fractionalSecondDigits, era: usedFormat.era, timeZoneName: usedFormat.timeZoneName, timeZone: utcOrNot } );
} else if( workingIn == 'seconds' ) {
dateFormatter = new Intl.DateTimeFormat( usedFormat.locales, { year: usedFormat.year, month: usedFormat.month, day: usedFormat.day, hour: usedFormat.hour, minute: usedFormat.minute, second: usedFormat.second, era: usedFormat.era, timeZoneName: usedFormat.timeZoneName, timeZone: utcOrNot } );
} else if( workingIn == 'minutes' || workingIn == 'hours' ) {
dateFormatter = new Intl.DateTimeFormat( usedFormat.locales, { year: usedFormat.year, month: usedFormat.month, day: usedFormat.day, hour: usedFormat.hour, minute: usedFormat.minute, era: usedFormat.era, timeZoneName: usedFormat.timeZoneName, timeZone: utcOrNot } );
} else if( workingIn == 'days' || workingIn == 'weeks' ) {
dateFormatter = new Intl.DateTimeFormat( usedFormat.locales, { year: usedFormat.year, month: usedFormat.month, day: usedFormat.day, era: usedFormat.era, timeZoneName: usedFormat.timeZoneName, timeZone: utcOrNot } );
} else if( workingIn == 'months' ) {
dateFormatter = new Intl.DateTimeFormat( usedFormat.locales, { year: usedFormat.year, month: usedFormat.month, era: usedFormat.era, timeZoneName: usedFormat.timeZoneName, timeZone: utcOrNot } );
} else {
dateFormatter = new Intl.DateTimeFormat( usedFormat.locales, { year: usedFormat.year, era: usedFormat.era, timeZoneName: usedFormat.timeZoneName, timeZone: utcOrNot } );
}
}
return dateFormatter.format(date);
}
if( workingIn == 'milliseconds' && usedFormat.fractionalSecondDigits ) {
dateString = '.' + padString( date[ 'get' + utcOrNot + 'Milliseconds' ](), 3, '0', true );
separatorM = separatorS = true;
}
if( ( workingIn == 'milliseconds' || workingIn == 'seconds' ) && usedFormat.second ) {
dateString = padString( date[ 'get' + utcOrNot + 'Seconds' ](), 2, '0' ) + dateString;
separatorM = separatorS = true;
}
if( separatorS ) {
dateString = ':' + dateString;
}
if( workingIn == 'milliseconds' || workingIn == 'seconds' || workingIn == 'minutes' || workingIn == 'hours' ) {
if( usedFormat.minute ) {
dateString = padString( date[ 'get' + utcOrNot + 'Minutes' ](), 2, '0' ) + dateString;
separatorM = true;
}
if( separatorM ) {
dateString = ':' + dateString;
}
if( usedFormat.hour ) {
dateString = padString( date[ 'get' + utcOrNot + 'Hours' ](), 2, '0' ) + dateString
}
if( dateString && ( usedFormat.day && usedFormat.month && usedFormat.year ) ) {
dateString = ' ' + dateString;
}
}
showAnyway = !dateString && ( !usedFormat.day || workingIn == 'months' || workingIn == 'years' ) && ( !usedFormat.month || workingIn == 'years' ) && !usedFormat.year;
if( ( workingIn != 'months' && workingIn != 'years' && usedFormat.day ) || showAnyway ) {
dateString = '-' + padString( date[ 'get' + utcOrNot + 'Date' ](), 2, '0' ) + dateString;
separatorMo = true;
}
if( ( workingIn != 'years' && usedFormat.month ) || showAnyway ) {
dateString = padString( date[ 'get' + utcOrNot + 'Month' ]() + 1, 2, '0' ) + dateString;
separatorMo = true;
}
if( separatorMo ) {
dateString = '-' + dateString;
}
year = date[ 'get' + utcOrNot + 'FullYear' ]();
if( year < 1 ) { year--; }
if( usedFormat.year || showAnyway ) {
dateString = padString( year, 4, '0' ) + dateString;
}
if( usedFormat.era && year < 1 ) {
dateString += ( dateString ? ' ' : '' ) + 'BCE';
}
if( !usedFormat.timeZoneName ) {
return dateString;
}
dateString += ( dateString ? ' ' : '' ) + 'UTC';
if( utcOrNot ) {
return dateString;
}
timezone = date.getTimezoneOffset();
dateString += ( ( timezone > 0 ) ? '-' : '+' );
timezone = Math.abs(timezone);
return dateString + padString( Math.floor( timezone / 60 ), 2, '0' ) + padString( timezone % 60, 2, '0' );
}
function orderOfMagnitude(val) {
val *= 1;
if( val == Number.POSITIVE_INFINITY || val == Number.NEGATIVE_INFINITY ) {
return Number.POSITIVE_INFINITY;
} else if( isNaN( val ) || !val ) {
return Number.NEGATIVE_INFINITY;
}
var numString = val + '';
var matches = numString.match(/^-?(\d*)(\.(0*)\d*)?(e((-|\+|)\d+))?$/i);
if( !matches ) {
return Number.NEGATIVE_INFINITY;
}
return matches[5] ? matches[5] * 1 : ( ( matches[1] != '0' ) ? matches[1].length : matches[3] ? -matches[3].length : 0 ) - 1;
}
function fixFloatingPoint(num) {
if( isNaN(num) || num == Number.POSITIVE_INFINITY || num == Number.NEGATIVE_INFINITY ) {
return num;
}
var numStr = num + '';
if( numStr.length < 14 || numStr.indexOf('.') == -1 ) {
return num;
}
var exponent = '', exponentIndex = numStr.indexOf('e');
if( exponentIndex != -1 ) {
exponent = numStr.substring(exponentIndex);
numStr = numStr.substring( 0, exponentIndex );
}
var sign = ( numStr.charAt(0) == '-' ) ? '-' : '';
if( sign ) {
numStr = '0' + numStr.substring(1);
} else {
numStr = '0' + numStr;
}
var leadingZeros = numStr.match(/^[0\.]*/);
var remainingLength = numStr.length - leadingZeros[0].length;
var allowedFigures = 14;
if( remainingLength <= allowedFigures ) {
return num;
}
if( numStr.substring( leadingZeros[0].length, leadingZeros[0].length + allowedFigures ).indexOf('.') != -1 ) {
allowedFigures++;
if( remainingLength == allowedFigures ) { return num; }
}
var afterAllowed = numStr.substring( leadingZeros[0].length + allowedFigures );
var rounded = Math.round( parseFloat( '.' + afterAllowed.replace( /\./, '' ) ) ) == 1;
afterAllowed = afterAllowed.replace( /[1-9]/g, '0' );
if( !rounded ) {
return parseFloat( sign + numStr.substring( 0, leadingZeros[0].length + allowedFigures ) + afterAllowed + exponent );
}
var tailStr = '', oneChar, charIndex;
for( charIndex = leadingZeros[0].length + allowedFigures - 1; charIndex >= 0; charIndex-- ) {
oneChar = numStr.charAt(charIndex);
if( oneChar == '9' ) {
tailStr = '0' + tailStr;
} else if( oneChar == '.' ) {
tailStr = oneChar + tailStr;
} else {
tailStr = ( parseInt(oneChar) + 1 ) + tailStr;
break;
}
}
return parseFloat( sign + numStr.substring( 0, charIndex ) + tailStr + afterAllowed + exponent );
}
function getGridStep(range) {
var magnitude = orderOfMagnitude(range), gridStep;
var scaledLimit = fixFloatingPoint( range / Math.pow(10,magnitude) );
if( scaledLimit == 1 ) {
gridStep = 1;
} else if( scaledLimit <= 2 ) {
gridStep = 2;
} else if( scaledLimit <= 5 ) {
gridStep = 5;
} else {
gridStep = 10;
}
gridStep *= Math.pow( 10, magnitude - 1 );
return fixFloatingPoint(gridStep);
}
function floorHour( date, utcOrNot ) {
date[ 'set' + utcOrNot + 'Minutes' ]( 0, 0, 0 );
if( date[ 'get' + utcOrNot + 'Minutes' ]() ) {
date[ 'set' + utcOrNot + 'Hours' ]( date[ 'get' + utcOrNot + 'Hours' ]() - 1, 0, 0, 0 );
}
return date;
}
function upToNextHour( date, utcOrNot ) {
date[ 'set' + utcOrNot + 'Minutes' ]( 0, 0, 0 );
if( date[ 'get' + utcOrNot + 'Minutes' ]() ) {
date.setTime( date.getTime() + ( 60 - date[ 'get' + utcOrNot + 'Minutes' ]() ) * 60 * 1000 );
} else {
date.setTime( date.getTime() + 60 * 60 * 1000 );
}
return date;
}
function incrementHour( date, utcOrNot ) {
date.setTime( date.getTime() + 60 * 60 * 1000 );
if( date[ 'get' + utcOrNot + 'Minutes' ]() ) {
date.setTime( date.getTime() + ( 60 - date[ 'get' + utcOrNot + 'Minutes' ]() ) * 60 * 1000 );
}
return date;
}
function checkValidRange( num, negativeInfinite, infinite, name ) {
if( typeof(num) != 'number' ) {
return num;
}
if( isNaN(num) ) {
console.warn( 'lineChart; chart of ' + yTitle + ( hasSecondYAxis ? ' and ' + yTitle2 : '' ) + ' vs ' + xTitle + ', ' + name + ' is NaN, and has been ignored. Continuing anyway, but this is a faulty configuration.' );
return undefined;
}
if( num > infinite || num < negativeInfinite ) {
console.warn( 'lineChart; chart of ' + yTitle + ( hasSecondYAxis ? ' and ' + yTitle2 : '' ) + ' vs ' + xTitle + ', ' + name + ' is ' + num + ', while the permitted range is ' + negativeInfinite + ' to ' + infinite + ', so the value has been forced within that range. Continuing anyway, but this is a faulty configuration.' );
return Math.max( negativeInfinite, Math.min( num, infinite ) );
}
return num;
}
function forcedIncrement( num, startIncrement ) {
if( isNaN(num) || num == Number.POSITIVE_INFINITY || num == Number.NEGATIVE_INFINITY ) {
return num;
}
var numIncremented = num;
do {
numIncremented += startIncrement;
startIncrement *= 2;
} while( numIncremented == num );
return numIncremented;
}
function getDashPattern( option, i ) {
if( !window.Array || !Array.isArray ) {
return [];
}
if( typeof(option) == 'number' ) {
return dashPatterns[ ( Math.abs(Math.round(option)) % dashPatterns.length ) || 0 ];
} else if( option && Array.isArray(option) ) {
return option;
} else if( i == undefined ) {
return [];
} else if( Array.isArray(i) ) {
return i;
} else {
return dashPatterns[ i % dashPatterns.length ];
}
}
var startedAt = new Date();
var canvas, context, measurer, computedStyle, dateFormatter, canvasCache, contextCache, lastPoint = {}, copied, copyContext;
var i, j;
var chromium = window.chrome, ie = document.documentMode, safari = window.safari || ( navigator.vendor && navigator.vendor.indexOf('Apple') != -1 );
var maxCanvas = chromium ? 16384 : ie ? 8192 : safari ? 14000 : 11180;
var maxCanvasCoord = ( chromium || safari ) ? 3.402823466385288e+38 : 5.649272421481508e+35;
var minCanvasCoord = chromium ? -1e+9 : safari ? -5.764607351235542e+17 : -5.649272421481508e+35;
var infinite = ( Number.MAX_VALUE / 2 ), negativeInfinite = -1 * ( Number.MAX_VALUE / 2 ), xInfinite, xNegativeInfinite;
var hasData = false, hasFirstYAxisData = false, hasSecondYAxisData = false;
var xMin = Number.POSITIVE_INFINITY, xMax = Number.NEGATIVE_INFINITY, yMin = Number.POSITIVE_INFINITY, yMax = Number.NEGATIVE_INFINITY;
var yMin2 = Number.POSITIVE_INFINITY, yMax2 = Number.NEGATIVE_INFINITY, swapper;
var addonText = '', divisor = 1, utcOrNot, workingIn, weekStartDay, tempDate, zeroDate;
var gridStepX, gridStepY, gridStepY2, currentGridX, currentGridY, currentGridY2;
var xStart, xEnd, xDiff, yStart, yEnd, yStart2, yEnd2, xStartText, xEndText, yStartText, yEndText, yStartText2, yEndText2;
var dataKeyCount = 0, xBandKeyCount = 0, yBandKeyCount = 0, keyCount, hasSecondYAxis = false;
var dataRelated, styles, strings, dateFormat, usedFormat, handlers, interaction, debug;
var xLinePad, yLinePad, labelGap, titleGap, labelLineHeight, dataKeyLineHeight, bandKeyLineHeight, dataKeyLineMiddle, bandKeyLineMiddle;
var keySizeHalf, keyGap, keyHeight, keyWidth, yPad, spaceForXAxisLabels, spaceForXAxisTitle, spaceForYAxis, spaceForYAxis2, spaceForXLabelsAndTitle;
var chartWidth, chartHeight, mainHeight, plotWidth, plotHeight, maxPointSize, maxPointOrLine, maxLineWidth, xFlatLabels;
var xGridParts = [], yGridParts = [], yGridParts2 = [], scalingFactorX, scalingFactorY, scalingFactorY2, whichOffset, whichFactor;
var yTitle2, gradient, labelCountX = 0, labelCountY = 0, labelCountY2 = 0, sectionStart, sectionEnd, itemPos, lastItemPos;
var lineWidth, showKey, pointSize, pointRenderer, tempText, xTextReplacer, yTextReplacer, yTextReplacer2, dataSetIndex, keyHidden;
var storedData = [], toolTip, toolTipBody, toolTipHead, firstTipTitle, secondTipTitle, lastHits = [], toolTipShowing = false;
var dashPatterns = [
[3,3],
[10,3],
[3,3,10,3],
[3,10],
[10,10],
[3,10,3,3]
];
if( !window.console ) {
window.console = { log: function () {}, warn: function () {}, error: function () {} };
}
if( typeof(yTitle) == 'object' ) {
if( yTitle.second != undefined ) {
hasSecondYAxis = true;
}
yTitle2 = hasSecondYAxis ? yTitle.second : yTitle.first;
yTitle = yTitle.first;
} else {
yTitle2 = yTitle;
}
if( !options ) { options = {}; }
if( options.debug && options.debug.time ) { console.info( 'LineChart; chart of ' + yTitle + ( hasSecondYAxis ? ' and ' + yTitle2 : '' ) + ' vs ' + xTitle + ' starting debugging rendering time. Finished declaring variables at ' + ( ( new Date() ).getTime() - startedAt.getTime() ) + 'ms.' ); }
dataRelated = {};
if( options.dataRelated ) {
for( i in options.dataRelated ) {
dataRelated[i] = options.dataRelated[i];
}
if( dataRelated.ySecondConverter ) {
dataRelated.ySecondConverter = {
offset: ( typeof(dataRelated.ySecondConverter.offset) == 'number' ) ? dataRelated.ySecondConverter.offset || 0 : 0,
ratio: ( typeof(dataRelated.ySecondConverter.ratio) == 'number' ) ? dataRelated.ySecondConverter.ratio || 1 : 1
};
}
}
dataRelated.dateTime = ( dataRelated.xType == 'datetime' || dataRelated.xType == 'datetimeutc' );
if( !dataRelated.dateTime && dataRelated.xType != 'duration' ) {
dataRelated.xType = 'number';
}
styles = options.styles || {};
styles = {
chartWidth: styles.chartWidth,
chartHeight: styles.chartHeight,
plotWidth: styles.plotWidth,
plotHeight: styles.plotHeight,
titleFontFamily: styles.titleFontFamily || 'serif',
titleFontSize: Math.round( Math.abs( styles.titleFontSize * 1 ) ) || 20,
xTitleColour: styles.xTitleColour || styles.xTitleColor || 'black',
yTitleColour: styles.yTitleColour || styles.yTitleColor || 'black',
ySecondTitleColour: styles.ySecondTitleColour || styles.ySecondTitleColor || 'black',
xLabels: styles.xLabels == undefined || ( ( styles.xLabels == 'off' ) ? false : styles.xLabels ),
yLabels: styles.yLabels == undefined || ( styles.yLabels && styles.yLabels != 'off' ),
ySecondLabels: styles.ySecondLabels == undefined || ( styles.ySecondLabels && styles.ySecondLabels != 'off' ),
labelFontSize: Math.round( Math.abs( styles.labelFontSize * 1 ) ) || 16,
labelFontFamily: styles.labelFontFamily || 'serif',
xLabelColour: styles.xLabelColour || styles.xLabelColor || 'black',
yLabelColour: styles.yLabelColour || styles.yLabelColor || 'black',
ySecondLabelColour: styles.ySecondLabelColour || styles.ySecondLabelColor || 'black',
keyFontSize: Math.round( Math.abs( styles.keyFontSize * 1 ) ) || 16,
keyFontFamily: styles.keyFontFamily || 'serif',
dataKeyHidden: styles.dataKeyHidden == 'no' ? false : styles.dataKeyHidden,
bandKeyHidden: styles.bandKeyHidden == 'no' ? false : styles.bandKeyHidden,
chartBackgroundColour: styles.chartBackgroundColour || styles.chartBackgroundColor || '',
plotTopColour: styles.plotTopColour || styles.plotTopColor || 'white',
plotBottomColour: styles.plotBottomColour || styles.plotBottomColor || '#eee',
xGridColour: styles.xGridColour || styles.xGridColor || '#ccc',
yGridColour: styles.yGridColour || styles.yGridColor || '#ccc',
ySecondGridColour: styles.ySecondGridColour || styles.ySecondGridColor || '#ccc',
xGridZeroColour: styles.xGridZeroColour || styles.xGridZeroColor || 'black',
yGridZeroColour: styles.yGridZeroColour || styles.yGridZeroColor || 'black',
ySecondGridZeroColour: styles.ySecondGridZeroColour || styles.ySecondGridZeroColor || 'black',
xGridLength: styles.xGridLength,
yGridLength: styles.yGridLength,
ySecondGridLength: styles.ySecondGridLength,
xGridTicks: styles.xGridTicks,
yGridTicks: styles.yGridTicks,
ySecondGridTicks: styles.ySecondGridTicks,
xGridDashPattern: getDashPattern(styles.xGridDashPattern),
yGridDashPattern: getDashPattern(styles.yGridDashPattern),
ySecondGridDashPattern: getDashPattern(styles.ySecondGridDashPattern),
xGridZeroDashPattern: getDashPattern(styles.xGridZeroDashPattern),
yGridZeroDashPattern: getDashPattern(styles.yGridZeroDashPattern),
ySecondGridZeroDashPattern: getDashPattern(styles.ySecondGridZeroDashPattern),
pointSize: ( typeof(styles.pointSize) == 'number' ) ? Math.abs( styles.pointSize ) : 5,
dataLineWidth: ( typeof(styles.dataLineWidth) == 'number' ) ? Math.abs(styles.dataLineWidth) || 0 : 1,
bandLineWidth: ( typeof(styles.bandLineWidth) == 'number' ) ? Math.abs(styles.bandLineWidth) || 0 : 1,
gridLineWidth: ( typeof(styles.gridLineWidth) == 'number' ) ? Math.abs(styles.gridLineWidth) || 0 : 1,
crosshairLineWidth: ( typeof(styles.crosshairLineWidth) == 'number' ) ? Math.abs(styles.crosshairLineWidth) || 0 : 1,
crosshairDashPattern: getDashPattern( styles.crosshairDashPattern, 4 )
};
strings = options.strings || {};
dateFormat = options.dateFormat || {};
handlers = options.handlers || {};
xTextReplacer = ( typeof(handlers.xTextReplacer) == 'function' ) ? handlers.xTextReplacer : null;
yTextReplacer = ( typeof(handlers.yTextReplacer) == 'function' ) ? handlers.yTextReplacer : null;
yTextReplacer2 = ( typeof(handlers.ySecondTextReplacer) == 'function' ) ? handlers.ySecondTextReplacer : null;
interaction = options.interaction || {};
debug = options.debug || {};
if( debug.time ) { console.info( 'Finished reading options at ' + ( ( new Date() ).getTime() - startedAt.getTime() ) + 'ms.' ); }
if( !parentElement || !parentElement.appendChild ) {
console.error( 'LineChart; chart of ' + yTitle + ( hasSecondYAxis ? ' and ' + yTitle2 : '' ) + ' vs ' + xTitle + ', needed a place to insert the chart, but was not provided with a parent element.', parentElement );
return null;
}
canvas = document.createElement('canvas');
if( !canvas.getContext || !canvas.getContext('2d') || !canvas.getContext('2d').measureText || !canvas.getContext('2d').drawImage || !canvas.getContext('2d').save || !window.Array || !Array.isArray ) {
parentElement.appendChild(document.createElement('p'));
parentElement.lastChild.className = 'linecharterror';
parentElement.lastChild.appendChild( document.createTextNode( strings.oldBrowser || 'Modern browsers can display a chart here. Your browser is too old. You should consider upgrading to the latest version of your browser, or using an alternative browser that has better standards support.' ) );
console.error( 'LineChart; chart of ' + yTitle + ' vs ' + xTitle + ' could not be drawn because the browser does not support basic canvas functions.' );
return null;
}
canvas.className = 'linechartcanvas';
if( debug.time ) { console.info( 'Finished checking canvas at ' + ( ( new Date() ).getTime() - startedAt.getTime() ) + 'ms.' ); }
xInfinite = dataRelated.dateTime ? 864e+13 : infinite;
xNegativeInfinite = dataRelated.dateTime ? -864e+13 : negativeInfinite;
if( !dataSets || !dataSets.length ) {
console.info( 'lineChart; chart of ' + yTitle + ( hasSecondYAxis ? ' and ' + yTitle2 : '' ) + ' vs ' + xTitle + ', no dataSets found. This is unusual, but continuing anyway.' );
dataSets = [];
}
maxPointSize = styles.pointSize;
maxLineWidth = styles.dataLineWidth;
for( i = 0; i < dataSets.length; i++ ) {
if( dataSets[i] ) {
keyHidden = dataSets[i].keyHidden == undefined ? styles.dataKeyHidden : dataSets[i].keyHidden == 'no' ? false : dataSets[i].keyHidden;
if( !keyHidden || ( keyHidden == 'empty' && dataSets[i].data && dataSets[i].data.length ) ) {
dataKeyCount++;
}
}
if( dataSets[i] && dataSets[i].data && dataSets[i].data.length ) {
if( typeof(dataSets[i].pointSize) == 'number' ) {
maxPointSize = Math.max( Math.abs(dataSets[i].pointSize) || 0, maxPointSize );
}
if( typeof(dataSets[i].lineWidth) == 'number' ) {
maxPointSize = Math.max( Math.abs(dataSets[i].lineWidth) || 0, maxLineWidth );
}
for( j = 0; j < dataSets[i].data.length; j++ ) {
if( dataSets[i].data[j] && typeof(dataSets[i].data[j].x) == 'number' && typeof(dataSets[i].data[j].y) == 'number' && !isNaN(dataSets[i].data[j].x) && !isNaN(dataSets[i].data[j].y) ) {
if( dataSets[i].data[j].x < xNegativeInfinite || dataSets[i].data[j].x > xInfinite || dataSets[i].data[j].y < negativeInfinite || dataSets[i].data[j].y > infinite ) {
console.warn( 'lineChart; chart of ' + yTitle + ( hasSecondYAxis ? ' and ' + yTitle2 : '' ) + ' vs ' + xTitle + ', dataSet ' + i + ' data point ' + j + ' has ' + dataSets[i].data[j].x + ' for x and ' + dataSets[i].data[j].y + ' for y, while the permitted range is ' + xNegativeInfinite + ' to ' + xInfinite + ' for x and ' + negativeInfinite + ' to ' + infinite + ' for y. The values have been forced within that range. Continuing anyway, but this data is beyond the abilities of JavaScript, so the chart may look wrong.' );
}
if( dataSets[i].data[j].x < xMin ) { xMin = dataSets[i].data[j].x; }
if( dataSets[i].data[j].x > xMax ) { xMax = dataSets[i].data[j].x; }
if( dataSets[i].yAxis == 'second' ) {
if( dataSets[i].data[j].y < yMin2 ) { yMin2 = dataSets[i].data[j].y; }
if( dataSets[i].data[j].y > yMax2 ) { yMax2 = dataSets[i].data[j].y; }
hasSecondYAxis = hasSecondYAxisData = true;
} else {
if( dataSets[i].data[j].y < yMin ) { yMin = dataSets[i].data[j].y; }
if( dataSets[i].data[j].y > yMax ) { yMax = dataSets[i].data[j].y; }
hasFirstYAxisData = true;
}
hasData = true;
} else {
console.warn( 'lineChart; chart of ' + yTitle + ( hasSecondYAxis ? ' and ' + yTitle2 : '' ) + ' vs ' + xTitle + ', dataSet ' + i + ' data point ' + j + ' does not have valid x and y properties. Continuing without this data point, but this is faulty data.' );
}
}
} else {
console.info( 'lineChart; chart of ' + yTitle + ( hasSecondYAxis ? ' and ' + yTitle2 : '' ) + ' vs ' + xTitle + ', dataSet ' + i + ' has no data, continuing anyway.' );
}
}
if( dataSets.length && !hasData ) {
console.info( 'lineChart; chart of ' + yTitle + ( hasSecondYAxis ? ' and ' + yTitle2 : '' ) + ' vs ' + xTitle + ' has no data in any data sets, continuing anyway.' );
}
if( hasData && !dataRelated.ySecondConverter ) {
if( ( hasSecondYAxis || typeof(dataRelated.ySecondStart) == 'number' || typeof(dataRelated.ySecondEnd) == 'number' || dataRelated.ySecondGrid != undefined ) && !hasSecondYAxisData ) {
console.info( 'lineChart; chart of ' + yTitle + ' and ' + yTitle2 + ' vs ' + xTitle + ' has a second Y axis, but no data associated with it. This is unusual, but continuing anyway.' );
}
if( hasSecondYAxisData && !hasFirstYAxisData ) {
console.info( 'lineChart; chart of ' + yTitle + ' and ' + yTitle2 + ' vs ' + xTitle + ' has data for the second Y axis, but no data for the first Y axis. This is unusual, but continuing anyway.' );
}
}
if( debug.time ) { console.info( 'Finished calculating data range at ' + ( ( new Date() ).getTime() - startedAt.getTime() ) + 'ms.' ); }
if( !xBands || !xBands.length ) { xBands = []; }
for( i = 0; i < xBands.length; i++ ) {
if( xBands[i] && typeof(xBands[i].start) == 'number' && typeof(xBands[i].end) == 'number' && !isNaN(xBands[i].start) && !isNaN(xBands[i].end) ) {
if( xBands[i].start < xNegativeInfinite || xBands[i].start > xInfinite || xBands[i].end < xNegativeInfinite || xBands[i].end > xInfinite ) {
console.warn( 'lineChart; chart of ' + yTitle + ( hasSecondYAxis ? ' and ' + yTitle2 : '' ) + ' vs ' + xTitle + ', xBand ' + i + ' has start ' + xBands[i].start + ' and end ' + xBands[i].end + ', while the permitted range is ' + xNegativeInfinite + ' to ' + xInfinite + '. The values have been forced within that range. Continuing anyway, but this data is beyond the abilities of JavaScript, so the chart may look wrong.' );
}
if( ( xBands[i].keyHidden == undefined && !styles.bandKeyHidden ) || xBands[i].keyHidden == 'no' || ( xBands[i].keyHidden != undefined && !xBands[i].keyHidden ) ) {
xBandKeyCount++;
}
if( xBands[i].end < xBands[i].start ) {
console.warn( 'lineChart; chart of ' + yTitle + ( hasSecondYAxis ? ' and ' + yTitle2 : '' ) + ' vs ' + xTitle + ', xBand ' + i + ' has start and end properties reversed. Continuing anyway, but this is a faulty configuration.' );
}
if( xBands[i].start < xMin ) { xMin = xBands[i].start; }
if( xBands[i].end < xMin ) { xMin = xBands[i].end; }
if( xBands[i].end > xMax ) { xMax = xBands[i].end; }
if( xBands[i].start > xMax ) { xMax = xBands[i].start; }
hasData = true;
} else {
console.warn( 'lineChart; chart of ' + yTitle + ( hasSecondYAxis ? ' and ' + yTitle2 : '' ) + ' vs ' + xTitle + ', xBand ' + i + ' does not have valid start and end properties. Continuing anyway, but this looks like faulty data.' );
}
}
if( !yBands || !yBands.length ) { yBands = []; }
for( i = 0; i < yBands.length; i++ ) {
if( yBands[i] && typeof(yBands[i].start) == 'number' && typeof(yBands[i].end) == 'number' && !isNaN(yBands[i].start) && !isNaN(yBands[i].end) ) {
if( yBands[i].start < negativeInfinite || yBands[i].start > infinite || yBands[i].end < negativeInfinite || yBands[i].end > infinite ) {
console.warn( 'lineChart; chart of ' + yTitle + ( hasSecondYAxis ? ' and ' + yTitle2 : '' ) + ' vs ' + xTitle + ', yBand ' + i + ' has start ' + yBands[i].start + ' and end ' + yBands[i].end + ', while the permitted range is ' + negativeInfinite + ' to ' + infinite + '. The values have been forced within that range. Continuing anyway, but this data is beyond the abilities of JavaScript, so the chart may look wrong.' );
}
if( ( yBands[i].keyHidden == undefined && !styles.bandKeyHidden ) || yBands[i].keyHidden == 'no' || ( yBands[i].keyHidden != undefined && !yBands[i].keyHidden ) ) {
yBandKeyCount++;
}
if( yBands[i].end < yBands[i].start ) {
console.warn( 'lineChart; chart of ' + yTitle + ( hasSecondYAxis ? ' and ' + yTitle2 : '' ) + ' vs ' + xTitle + ', yBand ' + i + ' has start and end properties reversed. Continuing anyway, but this is a faulty configuration.' );
}
if( yBands[i].yAxis == 'second' ) {
if( yBands[i].start < yMin2 ) { yMin2 = yBands[i].start; }
if( yBands[i].end < yMin2 ) { yMin2 = yBands[i].end; }
if( yBands[i].end > yMax2 ) { yMax2 = yBands[i].end; }
if( yBands[i].start > yMax2 ) { yMax2 = yBands[i].start; }
hasSecondYAxis = hasSecondYAxisData = true;
} else {
if( yBands[i].start < yMin ) { yMin = yBands[i].start; }
if( yBands[i].end < yMin ) { yMin = yBands[i].end; }
if( yBands[i].end > yMax ) { yMax = yBands[i].end; }
if( yBands[i].start > yMax ) { yMax = yBands[i].start; }
hasFirstYAxisData = true;
}
} else {
console.warn( 'lineChart; chart of ' + yTitle + ( hasSecondYAxis ? ' and ' + yTitle2 : '' ) + ' vs ' + xTitle + ', yBand ' + i + ' does not have valid start and end properties. Continuing anyway, but this looks like faulty data.' );
}
}
if( dataRelated.ySecondConverter && hasSecondYAxisData ) {
yMin2 = yMin2 / dataRelated.ySecondConverter.ratio + dataRelated.ySecondConverter.offset;
yMax2 = yMax2 / dataRelated.ySecondConverter.ratio + dataRelated.ySecondConverter.offset;
if( yMin2 < negativeInfinite || yMin2 > infinite || yMax2 < negativeInfinite || yMax2 > infinite ) {
console.warn( 'lineChart; chart of ' + yTitle + ( hasSecondYAxis ? ' and ' + yTitle2 : '' ) + ' vs ' + xTitle + ', dataSet ' + i + ' needs the first Y axis to range from ' + yMin2 + ' to ' + yMax2 + ' after applying the ySecondConverter, while the permitted range is ' + negativeInfinite + ' to ' + infinite + ' for y. The values have been forced within that range. Continuing anyway, but this data is beyond the abilities of JavaScript, so the chart may look wrong.' );
}
if( yMin2 < yMin ) { yMin = yMin2; }
if( yMax2 > yMax ) { yMax = yMax2; }
}
if( hasData ) {
xMin = Math.max( xNegativeInfinite, Math.min( xMin, xInfinite ) );
xMax = Math.max( xNegativeInfinite, Math.min( xMax, xInfinite ) );
} else {
xMin = xMax = 0;
}
if( hasFirstYAxisData ) {
yMin = Math.max( negativeInfinite, Math.min( yMin, infinite ) );
yMax = Math.max( negativeInfinite, Math.min( yMax, infinite ) );
} else {
yMin = yMax = 0;
}
if( hasSecondYAxisData ) {
yMin2 = Math.max( negativeInfinite, Math.min( yMin2, infinite ) );
yMax2 = Math.max( negativeInfinite, Math.min( yMax2, infinite ) );
} else {
yMin2 = yMax2 = 0;
}
if( debug.time ) { console.info( 'Finished calculating bands at ' + ( ( new Date() ).getTime() - startedAt.getTime() ) + 'ms.' ); }
dataRelated.xStart = checkValidRange( dataRelated.xStart, xNegativeInfinite, xInfinite, 'xStart' );
dataRelated.xEnd = checkValidRange( dataRelated.xEnd, xNegativeInfinite, xInfinite, 'xEnd' );
dataRelated.yStart = checkValidRange( dataRelated.yStart, negativeInfinite, infinite, 'yStart' );
dataRelated.yEnd = checkValidRange( dataRelated.yEnd, negativeInfinite, infinite, 'yEnd' );
if( !dataRelated.ySecondConverter ) {
dataRelated.yStart2 = checkValidRange( dataRelated.yStart2, negativeInfinite, infinite, 'ySecondStart' );
dataRelated.yEnd2 = checkValidRange( dataRelated.yEnd2, negativeInfinite, infinite, 'ySecondEnd' );
}
if( typeof(dataRelated.xStart) == 'number' && typeof(dataRelated.xEnd) == 'number' && dataRelated.xEnd < dataRelated.xStart ) {
console.warn( 'lineChart; chart of ' + yTitle + ( hasSecondYAxis ? ' and ' + yTitle2 : '' ) + ' vs ' + xTitle + ', xStart and xEnd options reversed. Continuing anyway, but this is a faulty configuration.' );
swapper = dataRelated.xEnd;
dataRelated.xEnd = dataRelated.xStart;
dataRelated.xStart = swapper;
}
if( typeof(dataRelated.yStart) == 'number' && typeof(dataRelated.yEnd) == 'number' && dataRelated.yEnd < dataRelated.yStart ) {
console.warn( 'lineChart; chart of ' + yTitle + ( hasSecondYAxis ? ' and ' + yTitle2 : '' ) + ' vs ' + xTitle + ', yStart and yEnd options reversed. Continuing anyway, but this is a faulty configuration.' );
swapper = dataRelated.yEnd;
dataRelated.yEnd = dataRelated.yStart;
dataRelated.yStart = swapper;
}
if( !dataRelated.ySecondConverter && typeof(dataRelated.ySecondStart) == 'number' && typeof(dataRelated.ySecondEnd) == 'number' && dataRelated.ySecondEnd < dataRelated.ySecondStart ) {
console.warn( 'lineChart; chart of ' + yTitle + ( hasSecondYAxis ? ' and ' + yTitle2 : '' ) + ' vs ' + xTitle + ', ySecondStart and ySecondEnd options reversed. Continuing anyway, but this is a faulty configuration.' );
swapper = dataRelated.ySecondEnd;
dataRelated.ySecondEnd = dataRelated.ySecondStart;
dataRelated.ySecondStart = swapper;
}
if( typeof(dataRelated.xEnd) == 'number' && dataRelated.xEnd < xMin ) { xMin = dataRelated.xEnd; }
if( typeof(dataRelated.xStart) == 'number' && dataRelated.xStart > xMax ) { xMax = dataRelated.xStart; }
if( typeof(dataRelated.yEnd) == 'number' && dataRelated.yEnd < yMin ) { yMin = dataRelated.yEnd; }
if( typeof(dataRelated.yStart) == 'number' && dataRelated.yStart > yMax ) { yMax = dataRelated.yStart; }
if( typeof(dataRelated.ySecondEnd) == 'number' && dataRelated.ySecondEnd < yMin2 ) { yMin2 = dataRelated.ySecondEnd; }
if( typeof(dataRelated.ySecondStart) == 'number' && dataRelated.ySecondStart > yMax2 ) { yMax2 = dataRelated.ySecondStart; }
xStart = fixFloatingPoint( ( typeof(dataRelated.xStart) == 'number' ) ? dataRelated.xStart : ( dataRelated.xStart == 'auto0' ) ? Math.min( xMin, 0 ) : xMin );
xEnd = fixFloatingPoint( ( typeof(dataRelated.xEnd) == 'number' ) ? dataRelated.xEnd : ( dataRelated.xEnd == 'auto0' ) ? Math.max( xMax, 0 ) : xMax );
yStart = fixFloatingPoint( ( typeof(dataRelated.yStart) == 'number' ) ? dataRelated.yStart : ( dataRelated.yStart == 'auto0' ) ? Math.min( yMin, 0 ) : yMin );
yEnd = fixFloatingPoint( ( typeof(dataRelated.yEnd) == 'number' ) ? dataRelated.yEnd : ( dataRelated.yEnd == 'auto0' ) ? Math.max( yMax, 0 ) : yMax );
yStart2 = fixFloatingPoint( ( typeof(dataRelated.ySecondStart) == 'number' ) ? dataRelated.ySecondStart : ( dataRelated.ySecondStart == 'auto0' ) ? Math.min( yMin2, 0 ) : yMin2 );
yEnd2 = fixFloatingPoint( ( typeof(dataRelated.ySecondEnd) == 'number' ) ? dataRelated.ySecondEnd : ( dataRelated.ySecondEnd == 'auto0' ) ? Math.max( yMax2, 0 ) : yMax2 );
if( typeof(dataRelated.ySecondStart) == 'number' || typeof(dataRelated.ySecondEnd) == 'number' ) {
hasSecondYAxis = true;
}
if( debug.time ) { console.info( 'Finished calculating data and band ranges at ' + ( ( new Date() ).getTime() - startedAt.getTime() ) + 'ms.' ); }
xDiff = xEnd - xStart;
if( dataRelated.xType == 'duration' ) {
if( xDiff <= 2 * 1000 ) {
workingIn = 'milliseconds';
addonText = ( 'milliseconds' in strings ) ? strings.milliseconds : ' (ms)';
} else if( xDiff <= 2 * 60 * 1000 ) {
workingIn = 'seconds';
addonText = ( 'seconds' in strings ) ? strings.seconds : ' (seconds)';
divisor = 1000;
} else if( xDiff <= 2 * 60 * 60 * 1000 ) {
workingIn = 'minutes';
addonText = ( 'minutes' in strings ) ? strings.minutes : ' (minutes)';
divisor = 60 * 1000;
} else if( xDiff <= 2 * 24 * 60 * 60 * 1000 ) {
workingIn = 'hours';
addonText = ( 'hours' in strings ) ? strings.hours : ' (hours)';
divisor = 60 * 60 * 1000;
} else if( xDiff <= 14 * 24 * 60 * 60 * 1000 ) {
workingIn = 'days';
addonText = ( 'days' in strings ) ? strings.days : ' (days)';
divisor = 24 * 60 * 60 * 1000;
} else if( xDiff <= 2 * 30.436875 * 24 * 60 * 60 * 1000 ) {
workingIn = 'weeks';
addonText = ( 'weeks' in strings ) ? strings.weeks : ' (weeks)';
divisor = 7 * 24 * 60 * 60 * 1000;
} else if( xDiff <= 2 * 365.4525 * 1000 * 24 * 60 * 60 ) {
workingIn = 'months';
addonText = ( 'months' in strings ) ? strings.months : ' (months)';
divisor = 30.436875 * 24 * 60 * 60 * 1000;
} else {
workingIn = 'years';
addonText = ( 'years' in strings ) ? strings.years : ' (years)';
divisor = 365.4525 * 1000 * 24 * 60 * 60;
}
xStart = fixFloatingPoint( xStart / divisor );
xEnd = fixFloatingPoint( xEnd / divisor );
if( typeof(dataRelated.xGrid) == 'number' && dataRelated.xGrid && dataRelated.xGrid != Number.POSITIVE_INFINITY && dataRelated.xGrid != Number.NEGATIVE_INFINITY ) {
dataRelated.xGrid = Math.max( xNegativeInfinite, Math.min( dataRelated.xGrid, xInfinite ) );
gridStepX = fixFloatingPoint( Math.abs( dataRelated.xGrid / divisor ) );
} else if( dataRelated.xGrid != 'none' ) {
gridStepX = getGridStep( xEnd - xStart || 1000 );
}
} else if( dataRelated.dateTime ) {
utcOrNot = ( dataRelated.xType == 'datetime' ) ? '' : 'UTC';
if( dataRelated.xGrid == 'none' ) {
workingIn = 'seconds';
} else {
if( typeof(dataRelated.xGrid) == 'number' ) {
console.warn( 'lineChart; chart of ' + yTitle + ( hasSecondYAxis ? ' and ' + yTitle2 : '' ) + ' vs ' + xTitle + ' tried to use numeric xGrid while xType was set to ' + dataRelated.xType + ', which is not possible. Continuing anyway, but this is a faulty configuration.' );
}
xStart = new Date(xStart);
tempDate = new Date(xStart);
xEnd = new Date(xEnd);
if( xDiff <= 2 * 60 * 1000 ) {
workingIn = 'seconds';
gridStepX = Math.max( getGridStep( xDiff / 1000 || 1000 ), 1 );
if( typeof(dataRelated.xStart) == 'number' ) {
currentGridX = tempDate.setUTCSeconds( Math.floor( tempDate.getUTCSeconds() / gridStepX ) * gridStepX, 0 );
if( xEnd.getTime() > tempDate.getTime() ) {
currentGridX = tempDate.setUTCSeconds( tempDate.getUTCSeconds() + gridStepX, 0 );
}
} else {
currentGridX = xStart.setUTCSeconds( Math.floor( xStart.getUTCSeconds() / gridStepX ) * gridStepX, 0 );
}
if( typeof(dataRelated.xEnd) != 'number' ) {
tempDate.setTime(xEnd.getTime());
tempDate.setUTCSeconds( Math.floor( tempDate.getUTCSeconds() / gridStepX ) * gridStepX, 0 );
if( xEnd.getTime() > tempDate.getTime() ) {
xEnd.setUTCSeconds( tempDate.getUTCSeconds() + gridStepX, 0 );
}
}
} else if( xDiff <= 2 * 60 * 60 * 1000 ) {
workingIn = 'minutes';
gridStepX = Math.max( getGridStep( xDiff / ( 60 * 1000 ) ), 1 );
if( typeof(dataRelated.xStart) == 'number' ) {
tempDate[ 'set' + utcOrNot + 'Minutes' ]( Math.floor( tempDate[ 'get' + utcOrNot + 'Minutes' ]() / gridStepX ) * gridStepX, 0, 0 );
if( xStart.getTime() > tempDate.getTime() ) {
currentGridX = tempDate.setUTCMinutes( tempDate.getUTCMinutes() + gridStepX, 0, 0 );
}
} else {
currentGridX = xStart[ 'set' + utcOrNot + 'Minutes' ]( Math.floor( xStart[ 'get' + utcOrNot + 'Minutes' ]() / gridStepX ) * gridStepX, 0, 0 );
}
if( typeof(dataRelated.xEnd) != 'number' ) {
tempDate.setTime(xEnd.getTime());
tempDate[ 'set' + utcOrNot + 'Minutes' ]( Math.floor( tempDate[ 'get' + utcOrNot + 'Minutes' ]() / gridStepX ) * gridStepX, 0, 0 );
if( xEnd.getTime() > tempDate.getTime() ) {
xEnd.setTime( tempDate.setUTCMinutes( tempDate.getUTCMinutes() + gridStepX, 0, 0 ) );
}
}
} else if( xDiff <= 2 * 24 * 60 * 60 * 1000 ) {
workingIn = 'hours';
gridStepX = 1;
if( typeof(dataRelated.xStart) == 'number' ) {
currentGridX = floorHour( tempDate, utcOrNot ).getTime();
if( xStart.getTime() > tempDate.getTime() ) {
currentGridX = incrementHour( tempDate, utcOrNot ).getTime();
}
} else {
currentGridX = floorHour( xStart, utcOrNot ).getTime();
}
if( typeof(dataRelated.xEnd) != 'number' && ( xEnd[ 'get' + utcOrNot + 'Minutes' ]() || xEnd[ 'get' + utcOrNot + 'Seconds' ]() || xEnd[ 'get' + utcOrNot + 'Milliseconds' ]() ) ) {
upToNextHour( xEnd, utcOrNot );
}
} else if( xDiff <= 14 * 24 * 60 * 60 * 1000 ) {
workingIn = 'days';
gridStepX = 1;
if( typeof(dataRelated.xStart) == 'number' ) {
currentGridX = tempDate[ 'set' + utcOrNot + 'Hours' ]( 0, 0, 0, 0 );
if( xStart.getTime() > tempDate.getTime() ) {
tempDate[ 'set' + utcOrNot + 'Date' ]( tempDate[ 'get' + utcOrNot + 'Date' ]() + 1 );
currentGridX = tempDate[ 'set' + utcOrNot + 'Hours' ]( 0, 0, 0, 0 );
}
} else {
currentGridX = xStart[ 'set' + utcOrNot + 'Hours' ]( 0, 0, 0, 0 );
}
if( typeof(dataRelated.xEnd) != 'number' && ( xEnd[ 'get' + utcOrNot + 'Hours' ]() || xEnd[ 'get' + utcOrNot + 'Minutes' ]() || xEnd[ 'get' + utcOrNot + 'Seconds' ]() || xEnd[ 'get' + utcOrNot + 'Milliseconds' ]() ) ) {
xEnd[ 'set' + utcOrNot + 'Date' ]( xEnd[ 'get' + utcOrNot + 'Date' ]() + 1 );
xEnd[ 'set' + utcOrNot + 'Hours' ]( 0, 0, 0, 0 );
}
} else if( xDiff <= 2 * 30.436875 * 24 * 60 * 60 * 1000 ) {
workingIn = 'weeks';
gridStepX = 1;
weekStartDay = ( ( Math.abs(Math.round(dataRelated.weekStartDay)) || 0 ) % 7 ) || 0;
if( typeof(dataRelated.xStart) == 'number' ) {
tempDate[ 'set' + utcOrNot + 'Date' ]( tempDate[ 'get' + utcOrNot + 'Date' ]() - ( 7 + tempDate[ 'get' + utcOrNot + 'Day' ]() - weekStartDay ) % 7 );
currentGridX = tempDate[ 'set' + utcOrNot + 'Hours' ]( 0, 0, 0, 0 );
if( xStart.getTime() > tempDate.getTime() ) {
tempDate[ 'set' + utcOrNot + 'Date' ]( tempDate[ 'get' + utcOrNot + 'Date' ]() + 7 );
currentGridX = tempDate[ 'set' + utcOrNot + 'Hours' ]( 0, 0, 0, 0 );
}
} else {
xStart[ 'set' + utcOrNot + 'Date' ]( xStart[ 'get' + utcOrNot + 'Date' ]() - ( 7 + xStart[ 'get' + utcOrNot + 'Day' ]() - weekStartDay ) % 7 );
currentGridX = xStart[ 'set' + utcOrNot + 'Hours' ]( 0, 0, 0, 0 );
}
if( typeof(dataRelated.xEnd) != 'number' && ( xEnd[ 'get' + utcOrNot + 'Day' ]() != weekStartDay || xEnd[ 'get' + utcOrNot + 'Hours' ]() || xEnd[ 'get' + utcOrNot + 'Minutes' ]() || xEnd[ 'get' + utcOrNot + 'Seconds' ]() || xEnd[ 'get' + utcOrNot + 'Milliseconds' ]() ) ) {
xEnd[ 'set' + utcOrNot + 'Date' ]( xEnd[ 'get' + utcOrNot + 'Date' ]() + ( 7 + weekStartDay - xEnd[ 'get' + utcOrNot + 'Day' ]() ) % 7 || 7 );
xEnd[ 'set' + utcOrNot + 'Hours' ]( 0, 0, 0, 0 );
}
} else if( xDiff <= 2 * 365.4525 * 1000 * 24 * 60 * 60 ) {
workingIn = 'months';
gridStepX = 1;
if( typeof(dataRelated.xStart) == 'number' ) {
tempDate[ 'set' + utcOrNot + 'Month' ]( tempDate[ 'get' + utcOrNot + 'Month' ](), 1 );
currentGridX = tempDate[ 'set' + utcOrNot + 'Hours' ]( 0, 0, 0, 0 );
if( xStart.getTime() > tempDate.getTime() ) {
tempDate[ 'set' + utcOrNot + 'Month' ]( tempDate[ 'get' + utcOrNot + 'Month' ]() + 1, 1 );
currentGridX = tempDate[ 'set' + utcOrNot + 'Hours' ]( 0, 0, 0, 0 );
}
} else {
xStart[ 'set' + utcOrNot + 'Month' ]( xStart[ 'get' + utcOrNot + 'Month' ](), 1 );
currentGridX = xStart[ 'set' + utcOrNot + 'Hours' ]( 0, 0, 0, 0 );
}
if( typeof(dataRelated.xEnd) != 'number' && ( xEnd[ 'get' + utcOrNot + 'Date' ]() != 1 || xEnd[ 'get' + utcOrNot + 'Hours' ]() || xEnd[ 'get' + utcOrNot + 'Minutes' ]() || xEnd[ 'get' + utcOrNot + 'Seconds' ]() || xEnd[ 'get' + utcOrNot + 'Milliseconds' ]() ) ) {
xEnd[ 'set' + utcOrNot + 'Month' ]( xEnd[ 'get' + utcOrNot + 'Month' ]() + 1, 1 );
xEnd[ 'set' + utcOrNot + 'Hours' ]( 0, 0, 0, 0 );
}
} else {
workingIn = 'years';
gridStepX = Math.max( getGridStep( xDiff / ( 365.4525 * 1000 * 24 * 60 * 60 ) ), 1 );
if( typeof(dataRelated.xStart) == 'number' ) {
tempDate[ 'set' + utcOrNot + 'FullYear' ]( Math.floor( tempDate[ 'get' + utcOrNot + 'FullYear' ]() / gridStepX ) * gridStepX, 0, 1 );
currentGridX = tempDate[ 'set' + utcOrNot + 'Hours' ]( 0, 0, 0, 0 );
if( xStart.getTime() > tempDate.getTime() ) {
tempDate[ 'set' + utcOrNot + 'FullYear' ]( tempDate[ 'get' + utcOrNot + 'FullYear' ]() + gridStepX, 0, 1 );
currentGridX = tempDate[ 'set' + utcOrNot + 'Hours' ]( 0, 0, 0, 0 );
}
} else {
xStart[ 'set' + utcOrNot + 'FullYear' ]( Math.floor( xStart[ 'get' + utcOrNot + 'FullYear' ]() / gridStepX ) * gridStepX, 0, 1 );
currentGridX = xStart[ 'set' + utcOrNot + 'Hours' ]( 0, 0, 0, 0 );
}
if( typeof(dataRelated.xEnd) != 'number' ) {
tempDate.setTime(xEnd.getTime());
tempDate[ 'set' + utcOrNot + 'FullYear' ]( Math.floor( tempDate[ 'get' + utcOrNot + 'FullYear' ]() / gridStepX ) * gridStepX, 0, 1 );
tempDate[ 'set' + utcOrNot + 'Hours' ]( 0, 0, 0, 0 );
if( xEnd.getTime() > tempDate.getTime() ) {
xEnd[ 'set' + utcOrNot + 'FullYear' ]( tempDate[ 'get' + utcOrNot + 'FullYear' ]() + gridStepX, 0, 1 );
xEnd[ 'set' + utcOrNot + 'Hours' ]( 0, 0, 0, 0 );
}
}
}
xStart = xStart.getTime();
xEnd = xEnd.getTime();
if( isNaN(currentGridX) ) {
currentGridX = xNegativeInfinite;
}
if( isNaN(xStart) ) {
xStart = xNegativeInfinite;
}
if( isNaN(xEnd) ) {
xEnd = xInfinite;
}
}
} else if( typeof(dataRelated.xGrid) == 'number' && dataRelated.xGrid && dataRelated.xGrid != Number.POSITIVE_INFINITY && dataRelated.xGrid != Number.NEGATIVE_INFINITY ) {
dataRelated.xGrid = Math.max( xNegativeInfinite, Math.min( dataRelated.xGrid, xInfinite ) );
gridStepX = fixFloatingPoint( Math.abs(dataRelated.xGrid) );
} else if( dataRelated.xGrid != 'none' ) {
gridStepX = getGridStep( xDiff || 1 );
}
if( debug.time ) { console.info( 'Finished calculating X grid stepping at ' + ( ( new Date() ).getTime() - startedAt.getTime() ) + 'ms.' ); }
if( !dataRelated.dateTime && dataRelated.xGrid != 'none' ) {
if( typeof(dataRelated.xStart) != 'number' ) {
xStart = fixFloatingPoint( Math.floor( fixFloatingPoint( xStart / gridStepX ) ) * gridStepX );
}
if( typeof(dataRelated.xEnd) != 'number' ) {
xEnd = fixFloatingPoint( Math.ceil( fixFloatingPoint( xEnd / gridStepX ) ) * gridStepX + 0 );
}
currentGridX = fixFloatingPoint( Math.ceil( fixFloatingPoint( xStart / gridStepX ) ) * gridStepX + 0 );
}
xStart = Math.max( xNegativeInfinite, Math.min( xStart, xInfinite ) );
xEnd = Math.max( xNegativeInfinite, Math.min( xEnd, xInfinite ) );
currentGridX = Math.max( xNegativeInfinite, Math.min( currentGridX, xInfinite ) );
if( xStart == xEnd ) {
if( ( typeof(dataRelated.xEnd) != 'number' || typeof(dataRelated.xStart) == 'number' ) && xEnd != xInfinite ) {
xEnd = ( dataRelated.xType == 'duration' || dataRelated.dateTime ) ? xEnd + 1000 : forcedIncrement( xEnd, 1 );
} else {
xStart = ( dataRelated.xType == 'duration' || dataRelated.dateTime ) ? xStart - 1000 : forcedIncrement( xStart, -1 );
if( dataRelated.xGrid != 'none' ) {
currentGridX = ( dataRelated.xType == 'duration' || dataRelated.dateTime ) ? currentGridX - 1000 : forcedIncrement( currentGridX, -1 );
}
}
}
if( debug.time ) { console.info( 'Finished expanding data range to match grid at ' + ( ( new Date() ).getTime() - startedAt.getTime() ) + 'ms.' ); }
if( typeof(dataRelated.yGrid) == 'number' && dataRelated.yGrid && dataRelated.yGrid != Number.POSITIVE_INFINITY && dataRelated.yGrid != Number.NEGATIVE_INFINITY ) {
dataRelated.yGrid = Math.max( negativeInfinite, Math.min( dataRelated.yGrid, infinite ) );
gridStepY = fixFloatingPoint( Math.abs(dataRelated.yGrid) );
} else if( dataRelated.yGrid != 'none' ) {
gridStepY = getGridStep( ( yEnd - yStart ) || 1 );
}
if( dataRelated.yGrid != 'none' ) {
if( typeof(dataRelated.yStart) != 'number' ) {
yStart = fixFloatingPoint( Math.floor( fixFloatingPoint( yStart / gridStepY ) ) * gridStepY );
}
if( typeof(dataRelated.yEnd) != 'number' ) {
yEnd = fixFloatingPoint( Math.ceil( fixFloatingPoint( yEnd / gridStepY ) ) * gridStepY + 0 );
}
currentGridY = fixFloatingPoint( Math.ceil( fixFloatingPoint( yStart / gridStepY ) ) * gridStepY + 0 );
}
yStart = Math.max( negativeInfinite, Math.min( yStart, infinite ) );
yEnd = Math.max( negativeInfinite, Math.min( yEnd, infinite ) );
currentGridX = Math.max( xNegativeInfinite, Math.min( currentGridX, xInfinite ) );
if( yStart == yEnd ) {
if( ( typeof(dataRelated.yEnd) != 'number' || typeof(dataRelated.yStart) == 'number' ) && yEnd != infinite ) {
yEnd = forcedIncrement( yEnd, 1 );
} else {
yStart = forcedIncrement( yStart, -1 );
if( dataRelated.yGrid != 'none' ) {
currentGridY = forcedIncrement( currentGridY, -1 );
}
}
}
if( debug.time ) { console.info( 'Finished calculating Y grid stepping at ' + ( ( new Date() ).getTime() - startedAt.getTime() ) + 'ms.' ); }
if( dataRelated.ySecondConverter ) {
hasSecondYAxis = true;
if( typeof(dataRelated.ySecondStart) == 'number' || typeof(dataRelated.ySecondEnd) == 'number' ) {
console.warn( 'lineChart; chart of ' + yTitle + ' and ' + yTitle2 + ' vs ' + xTitle + ', ySecondStart and ySecondEnd options cannot be used in combination with the ySecondConverter option, and have been ignored. Continuing anyway, but this is a faulty configuration.' );
dataRelated.ySecondStart = dataRelated.ySecondEnd = undefined;
}
yStart2 = ( yStart - dataRelated.ySecondConverter.offset ) * dataRelated.ySecondConverter.ratio;
yEnd2 = ( yEnd - dataRelated.ySecondConverter.offset ) * dataRelated.ySecondConverter.ratio;
yStart2 = checkValidRange( yStart2, negativeInfinite, infinite, 'calculated second Y axis start after applying converter' ) || 0;
yEnd2 = checkValidRange( yEnd2, negativeInfinite, infinite, 'calculated second Y axis end after applying converter' ) || 1;
}
if( typeof(dataRelated.ySecondGrid) == 'number' && dataRelated.ySecondGrid && dataRelated.ySecondGrid != Number.POSITIVE_INFINITY && dataRelated.ySecondGrid != Number.NEGATIVE_INFINITY ) {
dataRelated.ySecondGrid = Math.max( negativeInfinite, Math.min( dataRelated.ySecondGrid, infinite ) );
gridStepY2 = fixFloatingPoint( Math.abs(dataRelated.ySecondGrid) );
hasSecondYAxis = true;
} else if( dataRelated.ySecondGrid != 'none' ) {
gridStepY2 = getGridStep( ( yEnd2 > yStart2 ? yEnd2 - yStart2 : yStart2 - yEnd2 ) || 1 );
if( dataRelated.ySecondGrid != undefined ) {
hasSecondYAxis = true;
}
}
if( dataRelated.ySecondGrid != 'none' ) {
if( typeof(dataRelated.ySecondStart) != 'number' && !dataRelated.ySecondConverter ) {
yStart2 = fixFloatingPoint( Math.floor( fixFloatingPoint( yStart2 / gridStepY2 ) ) * gridStepY2 );
}
if( typeof(dataRelated.ySecondEnd) != 'number' && !dataRelated.ySecondConverter ) {
yEnd2 = fixFloatingPoint( Math.ceil( fixFloatingPoint( yEnd2 / gridStepY2 ) ) * gridStepY2 + 0 );
}
if( yStart2 > yEnd2 ) {
gridStepY2 *= -1;
currentGridY2 = fixFloatingPoint( Math.floor( fixFloatingPoint( yStart2 / gridStepY2 ) ) * gridStepY2 );
} else {
currentGridY2 = fixFloatingPoint( Math.ceil( fixFloatingPoint( yStart2 / gridStepY2 ) ) * gridStepY2 + 0 );
}
}
yStart2 = Math.max( negativeInfinite, Math.min( yStart2, infinite ) );
yEnd2 = Math.max( negativeInfinite, Math.min( yEnd2, infinite ) );
currentGridY2 = Math.max( negativeInfinite, Math.min( currentGridY2, infinite ) );
if( yStart2 == yEnd2 ) {
if( ( typeof(dataRelated.ySecondEnd) != 'number' || typeof(dataRelated.ySecondStart) == 'number' ) && yEnd2 != infinite ) {
yEnd2 = forcedIncrement( yEnd2, 1 );
} else {
yStart2 = forcedIncrement( yStart2, -1 );
if( dataRelated.ySecondGrid != 'none' ) {
currentGridY2 = forcedIncrement( currentGridY2, -1 );
}
}
}
if( debug.time ) { console.info( 'Finished calculating second Y grid stepping at ' + ( ( new Date() ).getTime() - startedAt.getTime() ) + 'ms.' ); }
if( xStart == xEnd || isNaN( xEnd - xStart ) || xEnd - xStart == Number.POSITIVE_INFINITY || yStart == yEnd || isNaN( yEnd - yStart ) || yEnd - yStart == Number.POSITIVE_INFINITY || yStart2 == yEnd2 || isNaN( yEnd2 - yStart2 ) || yEnd2 - yStart2 == Number.POSITIVE_INFINITY ) {
console.error( 'LineChart; chart of ' + yTitle + ( hasSecondYAxis ? ' and ' + yTitle2 : '' ) + ' vs ' + xTitle + ', ended up working in a number range beyond the ability of JavaScript, usually because one of the following numbers has reached Infinity or -Infinity, or NaN. The script tries to prevent this, but may have made a mistake. Please report this problem.', xStart, xEnd, yStart, yEnd, yStart2, yEnd2 );
parentElement.appendChild(document.createElement('p'));
parentElement.lastChild.className = 'linecharterror';
parentElement.lastChild.textContent = strings.numberLimit || 'It was not possible to draw the chart. JavaScript ran out of number precision, so the chart appears to start and end at the same number.';
return null;
}
context = canvas.getContext('2d');
context.save();
context.font = styles.labelFontSize + 'px ' + styles.labelFontFamily;
if( dataRelated.dateTime ) {
usedFormat = {
locales: dateFormat.locales || undefined,
year: dateFormat.year || 'numeric',
month: dateFormat.month || ( ( workingIn == 'months' ) ? 'short' : '2-digit' ),
day: dateFormat.day || '2-digit',
hour: dateFormat.hour || '2-digit',
minute: dateFormat.minute || '2-digit',
second: dateFormat.second || '2-digit',
fractionalSecondDigits: ( typeof(dateFormat.fractionalSecondDigits) == 'number' ) ? ( dateFormat.fractionalSecondDigits || undefined ) : 3,
timeZoneName: dateFormat.timeZoneName || 'short',
era: dateFormat.era || ( ( ( new Date(xStart) )['get' + utcOrNot + 'FullYear']() < 1 ) ? 'short' : undefined )
};
for( i in usedFormat ) {
if( i != 'locales' && dateFormat[i] == 'none' ) {
usedFormat[i] = undefined;
}
}
}
if( dataRelated.xType == 'datetime' ) {
zeroDate = ( new Date( 1970, 0, 1, 0, 0, 0, 0 ) ).setFullYear(0);
} else if( dataRelated.xType == 'datetimeutc' ) {
zeroDate = ( new Date('0000-01-01T00:00:00.000Z') ).getTime();
}
if( styles.xLabels ) {
if( dataRelated.dateTime ) {
xStartText = xTextReplacer ? xTextReplacer( xStart, { xType: dataRelated.xType, divisor: divisor, datePrecision: workingIn, dateFormat: dateFormat }, null, null ) : niceDate( xStart, utcOrNot, workingIn );
xEndText = xTextReplacer ? xTextReplacer( xEnd, { xType: dataRelated.xType, divisor: divisor, datePrecision: workingIn, dateFormat: dateFormat }, null, null ) : niceDate( xEnd, utcOrNot, workingIn );
} else {
xStartText = xTextReplacer ? xTextReplacer( xStart, { xType: dataRelated.xType, divisor: divisor, datePrecision: workingIn }, null, null ) : xStart;
xEndText = xTextReplacer ? xTextReplacer( xEndText, { xType: dataRelated.xType, divisor: divisor, datePrecision: workingIn }, null, null ) : xEnd;
}
sectionStart = context.measureText(xStartText).width;
sectionEnd = context.measureText(xEndText).width;
spaceForXAxisLabels = Math.max( sectionStart, sectionEnd );
xFlatLabels = sectionStart + sectionEnd;
} else {
xFlatLabels = spaceForXAxisLabels = 0;
}
if( dataRelated.xGrid != 'none' ) {
while( currentGridX <= xEnd ) {
if( dataRelated.dateTime ) {
tempText = xTextReplacer ? xTextReplacer( currentGridX, { xType: dataRelated.xType, divisor: divisor, datePrecision: workingIn, dateFormat: dateFormat }, null, null ) : niceDate( currentGridX, utcOrNot, workingIn );
} else {
tempText = xTextReplacer ? xTextReplacer( currentGridX, { xType: dataRelated.xType, divisor: divisor, datePrecision: workingIn }, null, null ) : currentGridX;
}
xGridParts.push({
itemPos: currentGridX - xStart,
grid0: dataRelated.dateTime ? ( currentGridX == zeroDate ) : !currentGridX,
text: tempText
});
if( styles.xLabels ) {
spaceForXAxisLabels = Math.max( spaceForXAxisLabels, context.measureText(tempText).width );
}
if( currentGridX + gridStepX == currentGridX ) {
console.warn( 'lineChart; chart of ' + yTitle + ( hasSecondYAxis ? ' and ' + yTitle2 : '' ) + ' vs ' + xTitle + ', JavaScript ran out of number precision when drawing the X axis grid lines, so that the chart lines appear to be at the same number as each other. Abandoning the attempt to draw grid lines.' );
break;
}
if( dataRelated.dateTime ) {
tempDate = new Date(currentGridX);
if( workingIn == 'seconds' ) {
currentGridX = tempDate.setUTCSeconds( tempDate.getUTCSeconds() + gridStepX, 0 );
} else if( workingIn == 'minutes' ) {
currentGridX = tempDate.setUTCMinutes( tempDate.getUTCMinutes() + gridStepX, 0, 0 );
} else if( workingIn == 'hours' ) {
currentGridX = incrementHour( tempDate, utcOrNot ).getTime();
} else if( workingIn == 'days' ) {
tempDate[ 'set' + utcOrNot + 'Date' ]( tempDate[ 'get' + utcOrNot + 'Date' ]() + 1 );
currentGridX = tempDate[ 'set' + utcOrNot + 'Hours' ]( 0, 0, 0, 0 );
} else if( workingIn == 'weeks' ) {
tempDate[ 'set' + utcOrNot + 'Date' ]( tempDate[ 'get' + utcOrNot + 'Date' ]() + 7 );
currentGridX = tempDate[ 'set' + utcOrNot + 'Hours' ]( 0, 0, 0, 0 );
} else if( workingIn == 'months' ) {
tempDate[ 'set' + utcOrNot + 'Month' ]( tempDate[ 'get' + utcOrNot + 'Month' ]() + 1, 1 );
currentGridX = tempDate[ 'set' + utcOrNot + 'Hours' ]( 0, 0, 0, 0 );
} else {
tempDate[ 'set' + utcOrNot + 'FullYear' ]( tempDate[ 'get' + utcOrNot + 'FullYear' ]() + gridStepX, 0, 1 );
currentGridX = tempDate[ 'set' + utcOrNot + 'Hours' ]( 0, 0, 0, 0 );
}
if( isNaN(tempDate) ) {
break;
}
} else {
currentGridX = fixFloatingPoint( currentGridX + gridStepX );
}
}
}
if( debug.time ) { console.info( 'Finished calculating X grid labels and positions at ' + ( ( new Date() ).getTime() - startedAt.getTime() ) + 'ms.' ); }
if( styles.yLabels ) {
yStartText = yTextReplacer ? yTextReplacer( yStart, null, { yAxis: 'first' }, null ) : yStart;
yEndText = yTextReplacer ? yTextReplacer( yEnd, null, { yAxis: 'first' }, null ) : yEnd;
spaceForYAxis = Math.max( context.measureText(yStartText).width, context.measureText(yEndText).width );
} else {
spaceForYAxis = 0;
}
for( ; dataRelated.yGrid != 'none' && currentGridY <= yEnd; currentGridY = fixFloatingPoint( gridStepY + currentGridY ) ) {
tempText = yTextReplacer ? yTextReplacer( currentGridY, null, { yAxis: 'first' }, null ) : currentGridY;
yGridParts.push({
itemPos: yEnd - currentGridY,
grid0: !currentGridY,
text: tempText
});
if( styles.yLabels ) {
spaceForYAxis = Math.max( spaceForYAxis, context.measureText(tempText).width );
}
if( currentGridY + gridStepY == currentGridY ) {
console.warn( 'lineChart; chart of ' + yTitle + ( hasSecondYAxis ? ' and ' + yTitle2 : '' ) + ' vs ' + xTitle + ', JavaScript ran out of number precision when drawing the Y axis grid lines, so that the chart lines appear to be at the same number as each other. Abandoning the attempt to draw grid lines.' );
break;
}
}
if( debug.time ) { console.info( 'Finished calculating Y grid labels and positions at ' + ( ( new Date() ).getTime() - startedAt.getTime() ) + 'ms.' ); }
if( styles.ySecondLabels && ( !dataRelated.ySecondConverter || dataRelated.ySecondGrid == 'none' ) ) {
yStartText2 = yTextReplacer2 ? yTextReplacer2( yStart, null, { yAxis: 'second' }, null ) : yStart2;
yEndText2 = yTextReplacer2 ? yTextReplacer2( yEnd, null, { yAxis: 'second' }, null ) : yEnd2;
spaceForYAxis2 = Math.max( context.measureText(yStartText2).width, context.measureText(yEndText2).width );
} else {
spaceForYAxis2 = 0;
}
for( ; dataRelated.ySecondGrid != 'none' && ( gridStepY2 < 0 ? yEnd2 : currentGridY2 ) <= ( gridStepY2 < 0 ? currentGridY2 : yEnd2 ); currentGridY2 = fixFloatingPoint( gridStepY2 + currentGridY2 ) ) {
tempText = yTextReplacer2 ? yTextReplacer2( currentGridY2, null, { yAxis: 'second' }, null ) : currentGridY2;
yGridParts2.push({
itemPos: yEnd2 - currentGridY2,
grid0: !currentGridY2,
text: tempText
});
if( styles.ySecondLabels ) {
spaceForYAxis2 = Math.max( spaceForYAxis2, context.measureText(tempText).width );
}
if( currentGridY2 + gridStepY2 == currentGridY2 ) {
console.warn( 'lineChart; chart of ' + yTitle + ( hasSecondYAxis ? ' and ' + yTitle2 : '' ) + ' vs ' + xTitle + ', JavaScript ran out of number precision when drawing the second Y axis grid lines, so that the chart lines appear to be at the same number as each other. Abandoning the attempt to draw grid lines.' );
break;
}
}
if( debug.time ) { console.info( 'Finished calculating second Y grid labels and positions at ' + ( ( new Date() ).getTime() - startedAt.getTime() ) + 'ms.' ); }
if( hasSecondYAxis ) {
if( styles.yGridTicks == undefined ) {
styles.yGridTicks = true;
}
if( styles.ySecondGridTicks == undefined ) {
styles.ySecondGridTicks = true;
}
}
labelGap = Math.round( 0.5 * styles.labelFontSize );
titleGap = Math.round( 0.2 * styles.titleFontSize + 0.2 * styles.labelFontSize );
labelLineHeight = Math.round( 1.2 * styles.labelFontSize );
bandKeyLineHeight = Math.round( 1.3 * styles.keyFontSize );
bandKeyLineMiddle = Math.round( bandKeyLineHeight / 2 );
dataKeyLineHeight = Math.max( bandKeyLineHeight, Math.round( 2 * maxPointSize + 2 ), maxLineWidth + 2 );
dataKeyLineMiddle = Math.round( dataKeyLineHeight / 2 );
keySizeHalf = Math.round( styles.keyFontSize / 2 );
keyGap = Math.round( 0.5 * styles.labelFontSize );
keyWidth = Math.max( 2 * Math.ceil(maxPointSize) + 10, 40 );
keyHeight = dataKeyCount * dataKeyLineHeight + ( xBandKeyCount + yBandKeyCount ) * bandKeyLineHeight;
xLinePad = ( dataRelated.xGrid != 'none' ) ? styles.gridLineWidth / 2 : 0;
yLinePad = ( dataRelated.yGrid != 'none' || ( hasSecondYAxis && dataRelated.ySecondGrid != 'none' ) ) ? styles.gridLineWidth / 2 : 0;
spaceForYAxis += styles.titleFontSize + titleGap + ( styles.yLabels ? labelGap : 0 ) + xLinePad + ( ( styles.yGridTicks && dataRelated.yGrid != 'none' ) ? styles.labelFontSize : 0 );
spaceForYAxis = Math.max( spaceForYAxis, maxPointSize, maxLineWidth / 2, Math.ceil( styles.labelFontSize / 2 ) );
spaceForYAxis2 += ( ( styles.ySecondGridTicks && dataRelated.ySecondGrid != 'none' ) ? styles.titleFontSize : 0 ) + xLinePad + ( styles.ySecondLabels ? labelGap : 0 ) + titleGap + styles.titleFontSize;
if( hasSecondYAxis ) {
spaceForYAxis2 = Math.max( spaceForYAxis2, maxPointSize, maxLineWidth / 2, Math.ceil( styles.labelFontSize / 2 ) );
} else {
spaceForYAxis2 = Math.max( maxPointSize, maxLineWidth / 2, Math.ceil( styles.labelFontSize / 2 ), xLinePad );
}
spaceForXAxisTitle = ( ( styles.xGridTicks && dataRelated.xGrid != 'none' ) ? styles.labelFontSize : 0 ) + yLinePad + 2 * titleGap + ( styles.xLabels ? labelGap : 0 ) + styles.titleFontSize;
yPad = Math.max( maxPointSize, maxLineWidth / 2, Math.round( styles.labelFontSize / 2 ), yLinePad );
if( debug.time ) { console.info( 'Finished preparing gaps at ' + ( ( new Date() ).getTime() - startedAt.getTime() ) + 'ms.' ); }
if( typeof(styles.chartWidth) == 'number' ) {
chartWidth = Math.round(styles.chartWidth);
if( styles.plotWidth != undefined ) {
console.warn( 'lineChart; chart of ' + yTitle + ( hasSecondYAxis ? ' and ' + yTitle2 : '' ) + ' vs ' + xTitle + ' has both chartWidth and plotWidth options set. Only one may be used at a time.' );
}
} else if( typeof(styles.plotWidth) == 'number' ) {
chartWidth = spaceForYAxis + Math.round(styles.plotWidth) + spaceForYAxis2;
} else {
measurer = document.createElement('div');
measurer.className = 'linechartcanvas';
measurer.innerHTML = ' ';
parentElement.appendChild(measurer);
computedStyle = getComputedStyle( measurer, null );
chartWidth = Math.floor( measurer.clientWidth - parseFloat(computedStyle.paddingLeft) - parseFloat(computedStyle.paddingRight) );
parentElement.removeChild(measurer);
computedStyle = null;
}
canvas.width = chartWidth = Math.max( spaceForYAxis + 2 * labelLineHeight + spaceForYAxis2, Math.min( chartWidth || 0, maxCanvas ) );
if( debug.time ) { console.info( 'Finished measuring chart width at ' + ( ( new Date() ).getTime() - startedAt.getTime() ) + 'ms.' ); }
plotWidth = chartWidth - ( spaceForYAxis + spaceForYAxis2 );
if( ( ( styles.xLabels && dataRelated.xGrid == 'none' ) || styles.xLabels == 'ends' ) && xFlatLabels + styles.labelFontSize < plotWidth ) {
spaceForXAxisLabels = styles.labelFontSize;
}
spaceForXLabelsAndTitle = spaceForXAxisTitle + spaceForXAxisLabels;
keyHeight = Math.max( maxPointSize - spaceForXLabelsAndTitle, maxLineWidth / 2 - spaceForXLabelsAndTitle, keyHeight );
if( typeof(styles.chartHeight) == 'number' ) {
plotHeight = styles.chartHeight - ( yPad + spaceForXLabelsAndTitle + keyHeight )
if( styles.plotHeight != undefined ) {
console.warn( 'lineChart; chart of ' + yTitle + ( hasSecondYAxis ? ' and ' + yTitle2 : '' ) + ' vs ' + xTitle + ' has both chartHeight and plotHeight options set. Only one may be used at a time.' );
}
} else if( typeof(styles.plotHeight) == 'number' ) {
plotHeight = styles.plotHeight;
} else {
plotHeight = Math.ceil( plotWidth * ( 5 / 8 ) );
}
plotHeight = Math.max( 2 * labelLineHeight, Math.min( plotHeight || 0, maxCanvas - ( yPad + spaceForXLabelsAndTitle + keyHeight ) ) );
mainHeight = yPad + plotHeight + spaceForXLabelsAndTitle;
scalingFactorX = plotWidth / ( xEnd - xStart );
scalingFactorY = plotHeight / ( yEnd - yStart );
scalingFactorY2 = plotHeight / ( yEnd2 - yStart2 );
if( debug.time ) { console.info( 'Finished calculating chart height at ' + ( ( new Date() ).getTime() - startedAt.getTime() ) + 'ms.' ); }
canvas.height = chartHeight = Math.ceil( mainHeight + keyHeight );
parentElement.appendChild(canvas);
if( debug.time ) { console.info( 'Finished appending the canvas at ' + ( ( new Date() ).getTime() - startedAt.getTime() ) + 'ms.' ); }
if( styles.chartBackgroundColour && styles.chartBackgroundColour != 'transparent' ) {
context.fillStyle = styles.chartBackgroundColour;
context.fillRect( 0, 0, chartWidth, chartHeight );
}
gradient = context.createLinearGradient( 0, yPad, 0, yPad + plotHeight );
gradient.addColorStop( 0, styles.plotTopColour );
gradient.addColorStop( 1, styles.plotBottomColour );
context.fillStyle = gradient;
context.fillRect( spaceForYAxis, yPad, plotWidth, plotHeight );
if( debug.time ) { console.info( 'Finished drawing chart background at ' + ( ( new Date() ).getTime() - startedAt.getTime() ) + 'ms.' ); }
for( i = 0, keyCount = 0; i < xBands.length; i++ ) {
if( xBands[i] && typeof(xBands[i].start) == 'number' && typeof(xBands[i].end) == 'number' ) {
sectionStart = ( Math.min( xBands[i].start, xBands[i].end ) / divisor - xStart ) * scalingFactorX;
sectionEnd = ( Math.max( xBands[i].end, xBands[i].start ) / divisor - xStart ) * scalingFactorX;
context.fillStyle = xBands[i].backgroundColour || xBands[i].backgroundColor || 'hsla(' + ( 360 * i / xBands.length ) + ',100%,60%,0.2)';
context.fillRect( spaceForYAxis + sectionStart, yPad, sectionEnd - sectionStart, plotHeight );
if( ( xBands[i].keyHidden == undefined && !styles.bandKeyHidden ) || ( xBands[i].keyHidden != undefined && !xBands[i].keyHidden ) ) {
itemPos = mainHeight + dataKeyCount * dataKeyLineHeight + keyCount * bandKeyLineHeight + bandKeyLineMiddle;
context.fillRect( spaceForYAxis, itemPos - keySizeHalf, keyWidth, styles.keyFontSize );
keyCount++;
}
}
}
for( i = 0, keyCount = 0; i < yBands.length; i++ ) {
if( yBands[i] && typeof(yBands[i].start) == 'number' && typeof(yBands[i].end) == 'number' ) {
if( yBands[i].yAxis == 'second' ) {
sectionStart = ( yEnd2 - Math.min( yBands[i].start, yBands[i].end ) ) * scalingFactorY2;
sectionEnd = ( yEnd2 - Math.max( yBands[i].end, yBands[i].start ) ) * scalingFactorY2;
} else {
sectionStart = ( yEnd - Math.min( yBands[i].start, yBands[i].end ) ) * scalingFactorY;
sectionEnd = ( yEnd - Math.max( yBands[i].end, yBands[i].start ) ) * scalingFactorY;
}
context.fillStyle = yBands[i].backgroundColour || yBands[i].backgroundColor || 'hsla(' + ( 360 * i / yBands.length ) + ',100%,60%,0.2)';
context.fillRect( spaceForYAxis, yPad + sectionEnd, plotWidth, sectionStart - sectionEnd );
if( ( yBands[i].keyHidden == undefined && !styles.bandKeyHidden ) || ( yBands[i].keyHidden != undefined && !yBands[i].keyHidden ) ) {
itemPos = mainHeight + dataKeyCount * dataKeyLineHeight + xBandKeyCount * bandKeyLineHeight + keyCount * bandKeyLineHeight + bandKeyLineMiddle;
context.fillRect( spaceForYAxis, itemPos - keySizeHalf, keyWidth, styles.keyFontSize );
keyCount++;
}
}
}
if( debug.time ) { console.info( 'Finished shading bands and band key at ' + ( ( new Date() ).getTime() - startedAt.getTime() ) + 'ms.' ); }
context.font = styles.labelFontSize + 'px ' + styles.labelFontFamily;
context.fillStyle = styles.yLabelColour;
context.textAlign = 'right';
context.textBaseline = 'middle';
context.lineWidth = styles.gridLineWidth;
context.lineCap = ( ( yGridParts.length || yGridParts2.length ) && xGridParts.length ) ? 'round' : 'butt';
sectionStart = ( styles.yGridTicks && dataRelated.yGrid != 'none' ) ? styles.labelFontSize : 0;
if( styles.yGridLength == 'short' ) {
sectionEnd = spaceForYAxis + Math.min( styles.labelFontSize, plotWidth );
} else if( typeof(styles.yGridLength) == 'number' ) {
sectionEnd = spaceForYAxis + plotWidth * Math.max( 0, Math.min( styles.yGridLength || 0, 100 ) ) / 100;
} else {
sectionEnd = spaceForYAxis + plotWidth;
}
for( i = 0; i < yGridParts.length; i++ ) {
itemPos = yPad + yGridParts[i].itemPos * scalingFactorY;
if( context.setLineDash ) {
context.setLineDash( yGridParts[i].grid0 ? styles.yGridZeroDashPattern : styles.yGridDashPattern );
}
context.beginPath();
context.strokeStyle = yGridParts[i].grid0 ? styles.yGridZeroColour : styles.yGridColour;
context.moveTo( spaceForYAxis - sectionStart, itemPos );
context.lineTo( sectionEnd, itemPos );
context.stroke();
if( styles.yLabels && ( !i || lastItemPos - itemPos >= labelLineHeight ) ) {
lastItemPos = itemPos;
labelCountY++;
context.fillText( yGridParts[i].text, spaceForYAxis - xLinePad - sectionStart - labelGap, itemPos );
}
}
if( styles.yLabels && ( dataRelated.yGrid == 'none' || labelCountY == 0 || ( labelCountY == 1 && yPad + plotHeight - lastItemPos >= lastItemPos - yPad ) ) ) {
context.fillText( yStartText, spaceForYAxis - xLinePad - sectionStart - labelGap, yPad + plotHeight );
}
if( styles.yLabels && ( dataRelated.yGrid == 'none' || labelCountY == 0 || ( labelCountY == 1 && yPad + plotHeight - lastItemPos < lastItemPos - yPad ) ) ) {
context.fillText( yEndText, spaceForYAxis - xLinePad - sectionStart - labelGap, yPad );
}
if( debug.time ) { console.info( 'Finished rendering Y grid labels and positions at ' + ( ( new Date() ).getTime() - startedAt.getTime() ) + 'ms.' ); }
if( hasSecondYAxis ) {
context.fillStyle = styles.ySecondLabelColour;
context.textAlign = 'left';
sectionStart = spaceForYAxis + plotWidth + ( ( styles.ySecondGridTicks && dataRelated.ySecondGrid != 'none' ) ? styles.labelFontSize : 0 );
if( typeof(styles.ySecondGridLength) == 'number' ) {
sectionEnd = spaceForYAxis + ( 100 - Math.max( 0, Math.min( styles.ySecondGridLength || 0, 100 ) ) ) / 100 * plotWidth;
} else {
sectionEnd = spaceForYAxis + plotWidth - Math.min( styles.labelFontSize, plotWidth );
}
for( i = 0; i < yGridParts2.length; i++ ) {
itemPos = yPad + yGridParts2[i].itemPos * scalingFactorY2;
if( context.setLineDash ) {
context.setLineDash( yGridParts2[i].grid0 ? styles.ySecondGridZeroDashPattern : styles.ySecondGridDashPattern );
}
context.beginPath();
context.strokeStyle = yGridParts2[i].grid0 ? styles.ySecondGridZeroColour : styles.ySecondGridColour;
context.moveTo( sectionStart, itemPos );
context.lineTo( sectionEnd, itemPos );
context.stroke();
if( styles.ySecondLabels && ( !i || lastItemPos - itemPos >= labelLineHeight ) ) {
lastItemPos = itemPos;
labelCountY2++;
context.fillText( yGridParts2[i].text, sectionStart + xLinePad + labelGap, itemPos );
}
}
if( styles.ySecondLabels && ( !dataRelated.ySecondConverter || dataRelated.ySecondGrid == 'none' ) && ( dataRelated.ySecondGrid == 'none' || labelCountY2 == 0 || ( labelCountY2 == 1 && yPad + plotHeight - lastItemPos >= lastItemPos - yPad ) ) ) {
context.fillText( yStartText2, sectionStart + xLinePad + labelGap, yPad + plotHeight );
}
if( styles.ySecondLabels && ( !dataRelated.ySecondConverter || dataRelated.ySecondGrid == 'none' ) && ( dataRelated.ySecondGrid == 'none' || labelCountY2 == 0 || ( labelCountY2 == 1 && yPad + plotHeight - lastItemPos < lastItemPos - yPad ) ) ) {
context.fillText( yEndText2, sectionStart + xLinePad + labelGap, yPad );
}
if( debug.time ) { console.info( 'Finished rendering second Y grid labels and positions at ' + ( ( new Date() ).getTime() - startedAt.getTime() ) + 'ms.' ); }
}
context.fillStyle = styles.xLabelColour;
context.textAlign = 'right';
sectionStart = yPad + plotHeight + ( ( styles.xGridTicks && dataRelated.xGrid != 'none' ) ? styles.labelFontSize : 0 );
if( styles.xGridLength == 'short' ) {
sectionEnd = yPad + plotHeight - Math.min( styles.labelFontSize, plotHeight );
} else if( typeof(styles.xGridLength) == 'number' ) {
sectionEnd = yPad + ( 100 - Math.max( 0, Math.min( styles.xGridLength || 0, 100 ) ) ) / 100 * plotHeight;
} else {
sectionEnd = yPad;
}
for( i = 0; i < xGridParts.length; i++ ) {
itemPos = spaceForYAxis + xGridParts[i].itemPos * scalingFactorX;
if( context.setLineDash ) {
context.setLineDash( xGridParts[i].grid0 ? styles.xGridZeroDashPattern : styles.xGridDashPattern );
}
context.beginPath();
context.strokeStyle = xGridParts[i].grid0 ? styles.xGridZeroColour : styles.xGridColour;
context.moveTo( itemPos, sectionStart );
context.lineTo( itemPos, sectionEnd );
context.stroke();
if( styles.xLabels && styles.xLabels != 'ends' && ( !i || itemPos - lastItemPos >= labelLineHeight ) ) {
lastItemPos = itemPos;
labelCountX++;
context.save();
context.rotate( Math.PI * -1 / 2 );
context.fillText( xGridParts[i].text, -1 * ( sectionStart + yLinePad + labelGap ), itemPos );
context.restore();
}
}
if( ( ( styles.xLabels && dataRelated.xGrid == 'none' ) || styles.xLabels == 'ends' ) && xFlatLabels + styles.labelFontSize < plotWidth ) {
context.textAlign = 'left';
context.textBaseline = 'top';
context.fillText( xStartText, spaceForYAxis, sectionStart + yLinePad + labelGap );
context.textAlign = 'right';
context.fillText( xEndText, spaceForYAxis + plotWidth, sectionStart + yLinePad + labelGap );
} else {
context.save();
context.rotate( Math.PI * -1 / 2 );
if( styles.xLabels && ( dataRelated.xGrid == 'none' || labelCountX == 0 || ( labelCountX == 1 && lastItemPos - spaceForYAxis >= spaceForYAxis + plotWidth - lastItemPos ) ) ) {
context.fillText( xStartText, -1 * ( sectionStart + yLinePad + labelGap ), spaceForYAxis );
}
if( styles.xLabels && ( dataRelated.xGrid == 'none' || labelCountX == 0 || ( labelCountX == 1 && lastItemPos - spaceForYAxis < spaceForYAxis + plotWidth - lastItemPos ) ) ) {
context.fillText( xEndText, -1 * ( sectionStart + yLinePad + labelGap ), spaceForYAxis + plotWidth );
}
context.restore();
}
context.lineCap = 'butt';
if( debug.time ) { console.info( 'Finished rendering X grid labels and positions at ' + ( ( new Date() ).getTime() - startedAt.getTime() ) + 'ms.' ); }
context.font = styles.titleFontSize + 'px ' + styles.titleFontFamily;
context.textAlign = 'center';
context.textBaseline = 'top';
context.fillStyle = styles.xTitleColour;
context.fillText( xTitle + addonText, spaceForYAxis + ( plotWidth / 2 ), yPad + plotHeight + ( ( styles.xGridTicks && dataRelated.xGrid != 'none' ) ? styles.labelFontSize : 0 ) + ( styles.xLabels ? labelGap : 0 ) + spaceForXAxisLabels + titleGap + yLinePad );
context.save();
context.rotate( Math.PI * -1 / 2 );
context.fillStyle = styles.yTitleColour;
context.fillText( yTitle, -1 * ( yPad + ( plotHeight / 2 ) ), 0 );
if( hasSecondYAxis ) {
context.fillStyle = styles.ySecondTitleColour;
context.textBaseline = 'bottom';
context.fillText( yTitle2, -1 * ( yPad + ( plotHeight / 2 ) ), chartWidth );
}
context.restore();
if( debug.time ) { console.info( 'Finished rendering axis titles at ' + ( ( new Date() ).getTime() - startedAt.getTime() ) + 'ms.' ); }
context.font = styles.keyFontSize + 'px ' + styles.keyFontFamily;
context.textAlign = 'left';
context.textBaseline = 'middle';
for( i = 0, keyCount = 0; i < xBands.length; i++ ) {
if( xBands[i] && typeof(xBands[i].start) == 'number' && typeof(xBands[i].end) == 'number' ) {
lineWidth = ( typeof(xBands[i].lineWidth) == 'number' ) ? Math.abs(xBands[i].lineWidth) || 0 : styles.bandLineWidth;
context.fillStyle = context.strokeStyle = xBands[i].colour || xBands[i].color || 'hsla(' + ( 360 * i / xBands.length ) + ',100%,30%,0.9)';
showKey = ( xBands[i].keyHidden == undefined && !styles.bandKeyHidden ) || ( xBands[i].keyHidden != undefined && !xBands[i].keyHidden );
if( showKey ) {
itemPos = mainHeight + dataKeyCount * dataKeyLineHeight + keyCount * bandKeyLineHeight + bandKeyLineMiddle;
keyCount++;
}
if( lineWidth ) {
context.lineWidth = lineWidth;
sectionStart = ( xBands[i].start / divisor - xStart ) * scalingFactorX;
sectionEnd = ( xBands[i].end / divisor - xStart ) * scalingFactorX;
if( context.setLineDash ) {
context.setLineDash( getDashPattern( xBands[i].dashPattern, i ) );
}
context.beginPath();
context.moveTo( spaceForYAxis + sectionStart, yPad );
context.lineTo( spaceForYAxis + sectionStart, yPad + plotHeight );
context.stroke();
context.beginPath();
context.moveTo( spaceForYAxis + sectionEnd, yPad );
context.lineTo( spaceForYAxis + sectionEnd, yPad + plotHeight );
context.stroke();
if( showKey ) {
context.beginPath();
context.moveTo( spaceForYAxis, itemPos - keySizeHalf );
context.lineTo( spaceForYAxis, itemPos + keySizeHalf );
context.stroke();
context.beginPath();
context.moveTo( spaceForYAxis, itemPos - keySizeHalf );
context.lineTo( spaceForYAxis + keyWidth, itemPos + keySizeHalf );
context.stroke();
context.beginPath();
context.moveTo( spaceForYAxis + keyWidth, itemPos - keySizeHalf );
context.lineTo( spaceForYAxis + keyWidth, itemPos + keySizeHalf );
context.stroke();
}
}
if( showKey ) {
context.fillText( xBands[i].name, spaceForYAxis + keyWidth + keyGap, itemPos );
}
}
}
for( i = 0, keyCount = 0; i < yBands.length; i++ ) {
if( yBands[i] && typeof(yBands[i].start) == 'number' && typeof(yBands[i].end) == 'number' ) {
lineWidth = ( typeof(yBands[i].lineWidth) == 'number' ) ? Math.abs(yBands[i].lineWidth) || 0 : styles.bandLineWidth;
context.fillStyle = context.strokeStyle = yBands[i].colour || yBands[i].color || 'hsla(' + ( 360 * i / yBands.length ) + ',100%,30%,0.9)';
showKey = ( yBands[i].keyHidden == undefined && !styles.bandKeyHidden ) || ( yBands[i].keyHidden != undefined && !yBands[i].keyHidden );
if( showKey ) {
itemPos = mainHeight + dataKeyCount * dataKeyLineHeight + xBandKeyCount * bandKeyLineHeight + keyCount * bandKeyLineHeight + bandKeyLineMiddle;
keyCount++;
}
if( lineWidth ) {
context.lineWidth = lineWidth;
sectionStart = ( yEnd - yBands[i].start ) * scalingFactorY;
sectionEnd = ( yEnd - yBands[i].end ) * scalingFactorY;
if( context.setLineDash ) {
context.setLineDash( getDashPattern( yBands[i].dashPattern, i ) );
}
context.beginPath();
context.moveTo( spaceForYAxis, yPad + sectionStart );
context.lineTo( spaceForYAxis + plotWidth, yPad + sectionStart );
context.stroke();
context.beginPath();
context.moveTo( spaceForYAxis, yPad + sectionEnd );
context.lineTo( spaceForYAxis + plotWidth, yPad + sectionEnd );
context.stroke();
if( showKey ) {
context.beginPath();
context.moveTo( spaceForYAxis, itemPos - keySizeHalf );
context.lineTo( spaceForYAxis + keyWidth, itemPos - keySizeHalf );
context.stroke();
context.beginPath();
context.moveTo( spaceForYAxis, itemPos + keySizeHalf );
context.lineTo( spaceForYAxis + keyWidth, itemPos + keySizeHalf );
context.stroke();
}
}
if( showKey ) {
context.fillText( yBands[i].name, spaceForYAxis + keyWidth + keyGap, itemPos );
}
}
}
if( debug.time ) { console.info( 'Finished rendering band boundaries and key at ' + ( ( new Date() ).getTime() - startedAt.getTime() ) + 'ms.' ); }
if( typeof(handlers.pointRenderer) != 'function' ) {
canvasCache = document.createElement('canvas');
contextCache = canvasCache.getContext('2d')
}
maxPointOrLine = Math.max( maxPointSize, maxLineWidth / 2 );
for( i = 0, keyCount = 0; i < dataSets.length; i++ ) {
if( dataSets[i] ) {
pointSize = ( typeof(dataSets[i].pointSize) == 'number' ) ? Math.abs(dataSets[i].pointSize) || 0 : styles.pointSize;
pointRenderer = ( typeof(dataSets[i].pointRenderer) == 'function' ) ? dataSets[i].pointRenderer : ( ( typeof(handlers.pointRenderer) == 'function' ) ? handlers.pointRenderer : null );
dataSetIndex = ( typeof(dataSets[i].pointSymbol) == 'number' ) ? Math.abs(Math.round(dataSets[i].pointSymbol)) || 0 : i;
storedData[i] = { name: dataSets[i].name, yAxis: ( dataSets[i].yAxis == 'second' ) ? 'second' : 'first', colour: dataSets[i].colour || dataSets[i].color || 'hsl(' + ( 360 * i / dataSets.length ) + ',70%,45%)', pointSize: pointSize, data: [] };
context.fillStyle = context.strokeStyle = storedData[i].colour;
context.lineWidth = lineWidth = ( typeof(dataSets[i].lineWidth) == 'number' ) ? Math.abs(dataSets[i].lineWidth) || 0 : styles.dataLineWidth;
if( lineWidth && context.setLineDash ) {
context.setLineDash(getDashPattern(dataSets[i].dashPattern));
}
if( dataSets[i] ) {
keyHidden = dataSets[i].keyHidden == undefined ? styles.dataKeyHidden : dataSets[i].keyHidden == 'no' ? false : dataSets[i].keyHidden;
if( !keyHidden || ( keyHidden == 'empty' && dataSets[i].data && dataSets[i].data.length ) ) {
itemPos = mainHeight + keyCount * dataKeyLineHeight + dataKeyLineMiddle;
context.fillText( dataSets[i].name, spaceForYAxis + keyWidth + keyGap, itemPos );
if( pointSize ) {
if( pointRenderer ) {
context.save();
context.beginPath();
context.textAlign = 'start';
context.textBaseline = 'alphabetic';
context.font = '10px sans-serif';
pointRenderer( context, spaceForYAxis + keyWidth / 2, itemPos, pointSize, dataSetIndex, i, -1 );
context.restore();
} else {
drawPoint( spaceForYAxis + keyWidth / 2, itemPos, pointSize, dataSetIndex, storedData[i].colour, ( dataSets[i].data && dataSets[i].data.length ) || 0 );
}
}
if( lineWidth ) {
context.beginPath();
context.moveTo( spaceForYAxis, itemPos );
context.lineTo( spaceForYAxis + keyWidth, itemPos );
context.stroke();
}
keyCount++;
if( debug.time ) { console.info( 'Finished data set ' + i + ' key at ' + ( ( new Date() ).getTime() - startedAt.getTime() ) + 'ms.' ); }
}
context.save();
context.beginPath();
context.rect( spaceForYAxis - ( maxPointOrLine + 1 ), yPad - ( maxPointOrLine + 1 ), plotWidth + 2 * maxPointOrLine + 2, plotHeight + 2 * maxPointOrLine + 2 );
context.clip();
if( debug.time ) { console.info( 'Finished clipping at ' + i + ' key at ' + ( ( new Date() ).getTime() - startedAt.getTime() ) + 'ms.' ); }
if( dataSets[i].yAxis == 'second' ) {
whichOffset = yEnd2;
whichFactor = scalingFactorY2;
} else {
whichOffset = yEnd;
whichFactor = scalingFactorY;
}
for( j = 0; j < dataSets[i].data.length; j++ ) {
if( dataSets[i].data[j] && typeof(dataSets[i].data[j].x) == 'number' && typeof(dataSets[i].data[j].y) == 'number' && !isNaN(dataSets[i].data[j].x) && !isNaN(dataSets[i].data[j].y) ) {
storedData[i].data[j] = {
posX: Math.max( xNegativeInfinite, Math.min( dataSets[i].data[j].x, xInfinite ) ),
posY: Math.max( negativeInfinite, Math.min( dataSets[i].data[j].y, infinite ) )
};
storedData[i].data[j].posX = spaceForYAxis + ( storedData[i].data[j].posX / divisor - xStart ) * scalingFactorX;
storedData[i].data[j].posY = yPad + ( whichOffset - storedData[i].data[j].posY ) * whichFactor;
storedData[i].data[j].posX = Math.max( minCanvasCoord, Math.min( storedData[i].data[j].posX, maxCanvasCoord ) );
storedData[i].data[j].posY = Math.max( minCanvasCoord, Math.min( storedData[i].data[j].posY, maxCanvasCoord ) );
if( interaction.hoverTooltip ) {
storedData[i].data[j].realX = dataSets[i].data[j].x;
storedData[i].data[j].x = dataSets[i].data[j].x / divisor;
storedData[i].data[j].y = dataSets[i].data[j].y;
}
if( pointSize ) {
if( pointRenderer ) {
context.save();
context.beginPath();
context.textAlign = 'start';
context.textBaseline = 'alphabetic';
context.font = '10px sans-serif';
pointRenderer( context, storedData[i].data[j].posX, storedData[i].data[j].posY, pointSize, dataSetIndex, i, j );
context.restore();
} else {
drawPoint( storedData[i].data[j].posX, storedData[i].data[j].posY, pointSize, dataSetIndex, storedData[i].colour, dataSets[i].data.length );
}
}
}
}
if( debug.time ) { console.info( 'Finished data set ' + i + ' points at ' + ( ( new Date() ).getTime() - startedAt.getTime() ) + 'ms.' ); }
if( lineWidth && storedData[i].data.length ) {
context.beginPath();
hasData = false;
for( j = 0; j < storedData[i].data.length; j++ ) {
if( storedData[i].data[j] ) {
if( hasData ) {
context.lineTo( storedData[i].data[j].posX, storedData[i].data[j].posY );
} else {
context.moveTo( storedData[i].data[j].posX, storedData[i].data[j].posY );
hasData = true;
}
}
}
context.stroke();
}
if( debug.time ) { console.info( 'Finished data set ' + i + ' line at ' + ( ( new Date() ).getTime() - startedAt.getTime() ) + 'ms.' ); }
if( !interaction.hoverTooltip ) {
storedData[i] = null;
}
context.restore();
}
}
}
context.restore();
context.beginPath();
dataSets = xBands = yBands = null;
if( debug.time ) { console.info( 'Finished rendering data and data key at ' + ( ( new Date() ).getTime() - startedAt.getTime() ) + 'ms.' ); }
if( interaction.hoverTooltip ) {
dateFormatter = undefined;
if( usedFormat ) {
usedFormat.month = dateFormat.month == 'none' ? undefined : ( dateFormat.month || '2-digit' );
}
canvas.style.cursor = 'crosshair';
if( styles.crosshairLineWidth ) {
copied = document.createElement('canvas');
copied.width = chartWidth;
copied.height = chartHeight;
copyContext = copied.getContext('2d');
copyContext.drawImage( canvas, 0, 0 );
}
toolTip = document.createElement('div');
toolTip.className = 'linecharttooltip' + ( parentElement.id ? ' linecharttooltip' + parentElement.id : '' );
toolTip.style.position = 'absolute';
toolTip.style.left = toolTip.style.top = toolTip.style.margin = '0';
toolTip.style.display = 'none';
toolTipBody = toolTip.appendChild(document.createElement('table'));
toolTipBody.style.whiteSpace = toolTip.style.whiteSpace = 'nowrap';
toolTipBody.appendChild(document.createElement('thead'));
toolTipHead = toolTipBody.firstChild.appendChild(document.createElement('tr'));
toolTipHead.appendChild(document.createElement('th'));
toolTipHead.cells[0].textContent = strings.dataSet || 'Data set';
toolTipHead.appendChild(document.createElement('th'));
toolTipHead.cells[1].textContent = xTitle + addonText;
firstTipTitle = document.createElement('th');
firstTipTitle.textContent = yTitle;
if( hasSecondYAxis ) {
secondTipTitle = document.createElement('th');
secondTipTitle.textContent = yTitle2;
}
toolTipBody = toolTipBody.appendChild(document.createElement('tbody'));
document.documentElement.appendChild(toolTip);
canvas.ontouchmove = function (e) {
if( e.touches.length == 1 ) {
this.onmousemove( e.touches[0], e );
} else {
this.onmouseout();
}
};
canvas.onmousemove = function ( e, wasTouch ) {
var allHits = [], subHits, element = canvas, canvasX = 0, canvasY = 0, diff, foundOne = false, i, j, hasNew = false, hasFirst = false, hasSecond = false, tempEl, tempRow, tempCell, xAxisInfo, yAxisInfo, pointInfo;
var extraSize = ( typeof(interaction.hoverTooltip) == 'number' ) ? interaction.hoverTooltip : 0;
do {
canvasX += element.offsetLeft;
canvasY += element.offsetTop;
element = element.offsetParent;
} while(element);
for( i = 0; i < storedData.length; i++ ) {
if( storedData[i] ) {
subHits = [];
for( j = 0; j < storedData[i].data.length; j++ ) {
if( storedData[i].data[j] ) {
diff = Math.sqrt( ( e.pageX - canvasX - storedData[i].data[j].posX ) * ( e.pageX - canvasX - storedData[i].data[j].posX ) + ( e.pageY - canvasY - storedData[i].data[j].posY ) * ( e.pageY - canvasY - storedData[i].data[j].posY ) );
if( diff < storedData[i].pointSize + extraSize ) {
subHits.push( { diff: diff, point: storedData[i].data[j], pointIndex: j } );
}
}
}
if( subHits.length ) {
subHits.sort( function ( a, b ) { return a.diff - b.diff; } );
allHits.push( { name: storedData[i].name, yAxis: storedData[i].yAxis, point: subHits[0].point, colour: storedData[i].colour, dataSetIndex: i, pointIndex: subHits[0].pointIndex, pointSize: storedData[i].pointSize, extraSize: extraSize } );
if( storedData[i].yAxis != 'second' || dataRelated.ySecondConverter ) {
hasFirst = true;
}
if( storedData[i].yAxis == 'second' || dataRelated.ySecondConverter ) {
hasSecond = true;
}
}
}
}
subHits = null;
if( allHits.length ) {
if( allHits.length != lastHits.length ) {
hasNew = true;
} else {
for( i = 0; i < allHits.length; i++ ) {
if( allHits[i].point != lastHits[i].point ) {
hasNew = true;
break;
}
}
}
if( hasNew ) {
lastHits = allHits;
if( hasFirst && !firstTipTitle.parentNode ) {
if( hasSecondYAxis && secondTipTitle.parentNode ) {
toolTipHead.insertBefore( firstTipTitle, secondTipTitle );
} else {
toolTipHead.appendChild(firstTipTitle);
}
} else if( !hasFirst && firstTipTitle.parentNode ) {
toolTipHead.removeChild(firstTipTitle);
}
if( hasSecondYAxis && hasSecond && !secondTipTitle.parentNode ) {
toolTipHead.appendChild(secondTipTitle);
} else if( hasSecondYAxis && !hasSecond && secondTipTitle.parentNode ) {
toolTipHead.removeChild(secondTipTitle);
}
toolTipBody.textContent = '';
for( i = 0; i < allHits.length; i++ ) {
if( !( 'xText' in allHits[i].point ) || !( 'yText' in allHits[i].point ) ) {
pointInfo = { x: allHits[i].point.realX, y: allHits[i].point.y, ySecondConverted: null, dataSetIndex: allHits[i].dataSetIndex, dataSetName: allHits[i].name, pointIndex: allHits[i].pointIndex };
}
if( !( 'xText' in allHits[i].point ) ) {
xAxisInfo = { xType: dataRelated.xType, divisor: divisor, datePrecision: workingIn, dateFormat: dataRelated.dateTime ? dateFormat : undefined }
if( xTextReplacer ) {
allHits[i].point.xText = xTextReplacer( allHits[i].point.x, xAxisInfo, null, pointInfo );
} else {
allHits[i].point.xText = dataRelated.dateTime ? niceDate( allHits[i].point.x, utcOrNot, 'milliseconds', dateFormat ) : allHits[i].point.x;
}
}
if( !( 'yText' in allHits[i].point ) ) {
yAxisInfo = { yAxis: allHits[i].yAxis };
if( allHits[i].yAxis == 'second' && yTextReplacer2 ) {
allHits[i].point.yText = yTextReplacer2( allHits[i].point.y, null, yAxisInfo, pointInfo );
} else if( allHits[i].yAxis == 'first' && yTextReplacer ) {
allHits[i].point.yText = yTextReplacer( allHits[i].point.y, null, yAxisInfo, pointInfo );
} else {
allHits[i].point.yText = allHits[i].point.y;
}
if( dataRelated.ySecondConverter ) {
if( allHits[i].yAxis == 'second' ) {
allHits[i].point.yMultipliedText = allHits[i].point.y / dataRelated.ySecondConverter.ratio + dataRelated.ySecondConverter.offset;
} else {
allHits[i].point.yMultipliedText = ( allHits[i].point.y - dataRelated.ySecondConverter.offset ) * dataRelated.ySecondConverter.ratio;
}
allHits[i].point.yMultipliedText = Math.max( negativeInfinite, Math.min( allHits[i].point.yMultipliedText, infinite ) );
pointInfo = { x: allHits[i].point.realX, y: allHits[i].point.y, ySecondConverted: { dataSetYAxis: ( allHits[i].yAxis == 'second' ) ? 'second' : 'first', raw: allHits[i].point.yMultipliedText }, dataSetIndex: allHits[i].dataSetIndex, dataSetName: allHits[i].name, pointIndex: allHits[i].pointIndex };
allHits[i].point.yMultipliedText = fixFloatingPoint(allHits[i].point.yMultipliedText);
if( allHits[i].yAxis == 'second' && yTextReplacer ) {
allHits[i].point.yMultipliedText = yTextReplacer( allHits[i].point.yMultipliedText, null, { yAxis: 'first' }, pointInfo );
} else if( allHits[i].yAxis != 'second' && yTextReplacer2 ) {
allHits[i].point.yMultipliedText = yTextReplacer2( allHits[i].point.yMultipliedText, null, { yAxis: 'second' }, pointInfo );
}
}
}
toolTipBody.appendChild(document.createElement('tr'));
toolTipBody.lastChild.appendChild(document.createElement('th'));
toolTipBody.lastChild.lastChild.style.color = allHits[i].colour;
toolTipBody.lastChild.lastChild.textContent = allHits[i].name;
toolTipBody.lastChild.appendChild(document.createElement('td'));
toolTipBody.lastChild.lastChild.textContent = allHits[i].point.xText;
toolTipBody.lastChild.appendChild(document.createElement('td'));
if( !hasFirst || allHits[i].yAxis != 'second' ) {
toolTipBody.lastChild.lastChild.textContent = allHits[i].point.yText;
} else if( dataRelated.ySecondConverter ) {
toolTipBody.lastChild.lastChild.textContent = allHits[i].point.yMultipliedText;
}
if( hasFirst && hasSecond ) {
toolTipBody.lastChild.appendChild(document.createElement('td'));
if( allHits[i].yAxis == 'second' ) {
toolTipBody.lastChild.lastChild.textContent = allHits[i].point.yText;
} else if( dataRelated.ySecondConverter ) {
toolTipBody.lastChild.lastChild.textContent = allHits[i].point.yMultipliedText;
}
}
}
}
if( ( hasNew || !toolTipShowing ) && styles.crosshairLineWidth ) {
context.save();
context.clearRect( 0, 0, chartWidth, chartHeight );
context.drawImage( copied, 0, 0 );
for( i = 0; i < allHits.length; i++ ) {
if( context.setLineDash ) {
context.setLineDash( styles.crosshairDashPattern );
}
context.lineWidth = styles.crosshairLineWidth;
context.strokeStyle = allHits[i].colour;
context.beginPath();
context.moveTo( allHits[i].point.posX, yPad + plotHeight );
context.lineTo( allHits[i].point.posX, yPad );
context.moveTo( spaceForYAxis + ( ( allHits[i].yAxis == 'second' ) ? plotWidth : 0 ), allHits[i].point.posY );
context.lineTo( spaceForYAxis + ( ( allHits[i].yAxis == 'second' ) ? 0 : plotWidth ), allHits[i].point.posY );
context.stroke();
}
context.restore();
}
if( !toolTipShowing ) {
toolTipShowing = true;
toolTip.style.display = '';
}
toolTip.style.left = ( ( e.pageX + 10 + toolTip.offsetWidth > window.innerWidth + window.pageXOffset - 20 ) ? Math.max( 0, e.pageX - 10 - toolTip.offsetWidth ) : e.pageX + 10 ) + 'px';
toolTip.style.top = ( ( e.pageY + 10 + toolTip.offsetHeight > window.innerHeight + window.pageYOffset - 20 ) ? Math.max( 0, e.pageY - 10 - toolTip.offsetHeight ) : e.pageY + 10 ) + 'px';
} else {
if( toolTipShowing ) {
toolTipShowing = false;
toolTip.style.display = 'none';
if( styles.crosshairLineWidth ) {
context.clearRect( 0, 0, chartWidth, chartHeight );
context.drawImage( copied, 0, 0 );
}
}
}
if( wasTouch && e.pageX - canvasX >= spaceForYAxis && e.pageX - canvasX <= spaceForYAxis + plotWidth && e.pageY - canvasY >= yPad && e.pageY - canvasY <= yPad + plotHeight ) {
wasTouch.preventDefault();
}
};
canvas.onmouseout = function (e) {
if( toolTipShowing ) {
toolTipShowing = false;
toolTip.style.display = 'none';
if( styles.crosshairLineWidth ) {
context.clearRect( 0, 0, chartWidth, chartHeight );
context.drawImage( copied, 0, 0 );
}
}
};
}
if( debug.time ) { console.info( 'Finished setting up tooltip and mouse handling at ' + ( ( new Date() ).getTime() - startedAt.getTime() ) + 'ms. Rendering complete.' ); }
return { canvas: canvas, workingCanvas: copied, tooltip: toolTip };
}