/* * Flot v0.9.0 * * Released under the MIT license. */ ( function( $ ) { function Plot( target_, data_, options_ ) { var series = []; var options = { // the color theme used for graphs colors: ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"], legend: { show: true, noColumns: 1, // number of colums in legend table labelFormatter: null, // fn: string -> string labelBoxBorderColor: "#ccc", // border color for the little label boxes container: null, // container (as jQuery object) to put legend in, null means default on top of graph position: "ne", // position of default legend container within plot margin: 5, // distance from grid edge to default legend container within plot backgroundColor: null, // null means auto-detect backgroundOpacity: 0.85 // set to 0 to avoid background }, xaxis: { label: null, showLabels: true, mode: null, // null or "time" min: null, // min. value to show, null means set automatically max: null, // max. value to show, null means set automatically autoscaleMargin: null, // margin in % to add if auto-setting min/max ticks: null, // either [1, 3] or [[1, "a"], 3] or (fn: axis info -> ticks) or app. number of ticks for auto-ticks tickFormatter: null, // fn: number -> string labelWidth: null, // size of tick labels in pixels labelHeight: null, // mode specific options tickDecimals: null, // no. of decimals, null means auto tickSize: null, // number or [number, "unit"] minTickSize: null, // number or [number, "unit"] monthNames: null, // list of names of months timeformat: null // format string to use }, yaxis: { label: null, showLabels: true, autoscaleMargin: 0.02 }, points: { show: false, radius: 3, lineWidth: 2, // in pixels fill: true, fillColor: "#ffffff" }, lines: { show: false, lineWidth: 2, // in pixels fill: false, fillColor: null }, bars: { show: false, lineWidth: 2, // in pixels barWidth: 1, // in units of the x axis fill: true, fillOpacity: 0.4, fillColor: null }, deltas: { show: false, color: { above: '#A00', below: '#00A', equal: '#D52' }, markerWidth: 3 }, grid: { showLines: 'both', showBorder: true, markers: [], // see API.txt for details labelFontSize: 16, // default is 16px font size for axis labels color: "#545454", // primary color used for outline and labels backgroundColor: null, // null for transparent, else color tickColor: "#dddddd", // color used for the ticks tickWidth: 1, // thickness of grid lines labelMargin: 3, // in pixels borderWidth: 2, clickable: null, hoverable: false, hoverColor: null, hoverFill: null, hoverRadius: null, mouseCatchingArea: 15, coloredAreas: null, // array of { x1, y1, x2, y2 } or fn: plot area -> areas coloredAreasColor: "#f4f4f4" }, hints: { show: false, showColorBox: true, showSeriesLabel: true, labelFormatter: defaultLabelFormatter, hintFormatter: defaultHintFormatter, backgroundColor: "#DDD", // null means auto-detect backgroundOpacity: 0.7, // set to 0 to avoid background borderColor: "#BBB" // set to 'transparent' for none }, selection: { snapToTicks: false, // boolean for if we should snap to ticks on selection mode: null, // one of null, "x", "y" or "xy" color: "#e8cfac" }, shadowSize: 4, sortData: true }; var canvas = null, overlay = null, eventHolder = null, ctx = null, octx = null, target = target_, xaxis = {}, yaxis = {}, plotOffset = { left: 0, right: 0, top: 0, bottom: 0}, canvasWidth = 0, canvasHeight = 0, plotWidth = 0, plotHeight = 0, hozScale = 0, vertScale = 0, hintDiv = null, hintBackground = null, lastMarker = null, // dedicated to storing data for buggy standard compliance cases workarounds = {}, // buffer constants RIGHT_SIDE_BUFFER = 10, BOTTOM_SIDE_BUFFER = 10; this.setData = setData; this.setupGrid = setupGrid; this.highlight = highlight; this.draw = draw; this.cleanup = cleanup; this.clearSelection = clearSelection; this.setSelection = setSelection; this.getCanvas = function () { return canvas; }; this.getPlotOffset = function () { return plotOffset; }; this.getData = function () { return series; }; this.getAxes = function () { return { xaxis: xaxis, yaxis: yaxis }; }; // initialize $.extend( true, options, options_ ); setData( data_ ); constructCanvas(); setupGrid(); draw(); // kill hints and highlighted points when the mouse leaves the graph if( options.grid.hoverable ) $(target).mouseout( cleanup ); function setData( d ) { series = parseData(d); fillInSeriesOptions(); processData(); } // normalize the data given to the call to $.plot. If we're // going to be monitoring mousemove's then sort the data function parseData( d ) { function sortData( a, b ) { if( !a || !b ) return 0; if( a.x > b.x ) return 1; else if( a.x < b.x ) return -1; else return 0; } var res = []; for( var i = 0; i < d.length; ++i ) { var s = {}; if( d[i].data ) { for( var v in d[i] ) s[v] = d[i][v]; } else { s.data = d[i]; } res.push( s ); } // normalize the old style [x,y] data format for( var i in res ) { for( var j in res[i].data ) { var datapoint = res[i].data[j]; if( datapoint != null && datapoint.x == undefined ) { res[i].data[j] = { x: datapoint[0], y: datapoint[1] }; } } if( options.sortData ) res[i].data.sort( sortData ); } return res; } function fillInSeriesOptions() { var i; // collect what we already got of colors var neededColors = series.length; var usedColors = []; var assignedColors = []; for( i = 0; i < series.length; ++i ) { var sc = series[i].color; if( sc != null ) { --neededColors; if( typeof sc == "number" ) { assignedColors.push( sc ); } else { usedColors.push( parseColor( series[i].color ) ); } } } // we might need to generate more colors if higher indices // are assigned for( i = 0; i < assignedColors.length; ++i ) { neededColors = Math.max( neededColors, assignedColors[i] + 1 ); } // produce colors as needed var colors = []; var variation = 0; i = 0; while( colors.length < neededColors ) { var c; if( options.colors.length == i ) { c = new Color( 100, 100, 100 ); } else { c = parseColor( options.colors[i] ); } // vary color if needed var sign = variation % 2 == 1 ? -1 : 1; var factor = 1 + sign * Math.ceil( variation / 2 ) * 0.2; c.scale( factor, factor, factor ); // FIXME: if we're getting to close to something else, // we should probably skip this one colors.push( c ); ++i; if( i >= options.colors.length ) { i = 0; ++variation; } } // fill in the options var colori = 0, s; for( i = 0; i < series.length; ++i ) { s = series[i]; // assign colors if( s.color == null ) { s.color = colors[colori].toString(); ++colori; } else if( typeof s.color == "number" ) { s.color = colors[s.color].toString(); } // copy the rest s.lines = $.extend(true, {}, options.lines, s.lines); s.points = $.extend(true, {}, options.points, s.points); s.bars = $.extend(true, {}, options.bars, s.bars); s.deltas = $.extend(true, {}, options.deltas, s.deltas); s.hints = $.extend(true, {}, options.hints, s.hints); if (s.shadowSize == null) s.shadowSize = options.shadowSize; } } function processData() { var top_sentry = Number.POSITIVE_INFINITY, bottom_sentry = Number.NEGATIVE_INFINITY; xaxis.datamin = yaxis.datamin = top_sentry; xaxis.datamax = yaxis.datamax = bottom_sentry; for( var i = 0; i < series.length; ++i ) { var data = series[i].data; for( var j = 0; j < data.length; ++j ) { if( data[j] == null ) continue; var x = data[j].x, y = data[j].y; // convert to number if( x == null || y == null || isNaN( x = +x ) || isNaN( y = +y ) ) { data[j] = null; // mark this point as invalid continue; } if( x < xaxis.datamin ) xaxis.datamin = x; if( x > xaxis.datamax ) xaxis.datamax = x; if( y < yaxis.datamin ) yaxis.datamin = y; if( y > yaxis.datamax ) yaxis.datamax = y; } } if( xaxis.datamin == top_sentry ) xaxis.datamin = 0; if( yaxis.datamin == top_sentry ) yaxis.datamin = 0; if( xaxis.datamax == bottom_sentry ) xaxis.datamax = 1; if( yaxis.datamax == bottom_sentry ) yaxis.datamax = 1; } function constructCanvas() { canvasWidth = target.width(); canvasHeight = target.height(); target.html( '' ).css( 'position', 'relative' ); if( canvasWidth <= 0 || canvasHeight <= 0 ) { throw "Invalid dimensions for plot, width = " + canvasWidth + ", height = " + canvasHeight; } // the canvas canvas = $('').appendTo( target ).get( 0 ); if( $.browser.msie ) { canvas = window.G_vmlCanvasManager.initElement( canvas ); } ctx = canvas.getContext( '2d' ); // overlay canvas for interactive features overlay = $('').appendTo( target ).get( 0 ); if( $.browser.msie ) { overlay = window.G_vmlCanvasManager.initElement( overlay ); } octx = overlay.getContext( '2d' ); // we include the canvas in the event holder too, because IE 7 // sometimes has trouble with the stacking order eventHolder = $( [ overlay, canvas ] ); // bind events if( options.selection.mode != null ) { eventHolder.mousedown( onMouseDown ).mousemove( onMouseMove ); } if( options.grid.hoverable ) { eventHolder.mousemove( onMouseMove ); } if( options.grid.clickable ) { eventHolder.click( onClick ); } } function setupGrid() { // x axis setRange( xaxis, options.xaxis ); prepareTickGeneration( xaxis, options.xaxis ); setTicks( xaxis, options.xaxis ); extendXRangeIfNeededByBar(); // y axis setRange( yaxis, options.yaxis ); prepareTickGeneration( yaxis, options.yaxis ); setTicks( yaxis, options.yaxis ); setSpacing(); insertTickLabels(); insertLegend(); insertAxisLabels(); } function setRange( axis, axisOptions ) { var min = axisOptions.min != null ? axisOptions.min : axis.datamin; var max = axisOptions.max != null ? axisOptions.max : axis.datamax; if( max - min == 0.0 ) { // degenerate case var widen; if( max == 0.0 ) widen = 1.0; else widen = 0.01; min -= widen; max += widen; } else { // consider autoscaling var margin = axisOptions.autoscaleMargin; if( margin != null ) { if( axisOptions.min == null ) { min -= ( max - min ) * margin; // make sure we don't go below zero if all values // are positive if( min < 0 && axis.datamin >= 0 ) min = 0; } if( axisOptions.max == null ) { max += ( max - min ) * margin; if( max > 0 && axis.datamax <= 0 ) max = 0; } } } axis.min = min; axis.max = max; } function prepareTickGeneration( axis, axisOptions ) { // estimate number of ticks var noTicks; if( typeof axisOptions.ticks == "number" && axisOptions.ticks > 0 ) { noTicks = axisOptions.ticks; } else if( axis == xaxis ) { noTicks = canvasWidth / 100; } else { noTicks = canvasHeight / 60; } var delta = ( axis.max - axis.min ) / noTicks; var size, generator, unit, formatter, i, magn, norm; if( axisOptions.mode == "time" ) { // pretty handling of time function formatDate( d, fmt, monthNames ) { var leftPad = function( n ) { n = '' + n; return n.length == 1 ? '0' + n : n; }; var r = []; var escape = false; if( monthNames == null ) { monthNames = [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" ]; } for( var i = 0; i < fmt.length; ++i ) { var c = fmt.charAt( i ); if( escape ) { switch (c) { case 'h': c = "" + d.getUTCHours(); break; case 'H': c = leftPad( d.getUTCHours() ); break; case 'M': c = leftPad( d.getUTCMinutes() ); break; case 'S': c = leftPad( d.getUTCSeconds() ); break; case 'd': c = "" + d.getUTCDate(); break; case 'm': c = "" + ( d.getUTCMonth() + 1 ); break; case 'y': c = "" + d.getUTCFullYear(); break; case 'b': c = "" + monthNames[d.getUTCMonth()]; break; } r.push( c ); escape = false; } else { if( c == "%" ) escape = true; else r.push( c ); } } return r.join( '' ); } // map of app. size of time units in milliseconds var timeUnitSize = { "second": 1000, "minute": 60 * 1000, "hour": 60 * 60 * 1000, "day": 24 * 60 * 60 * 1000, "month": 30 * 24 * 60 * 60 * 1000, "year": 365.2425 * 24 * 60 * 60 * 1000 }; // the allowed tick sizes, after 1 year we use // an integer algorithm var spec = [ [ 1, "second" ], [ 2, "second" ], [ 5, "second" ], [ 10, "second" ], [ 30, "second" ], [ 1, "minute" ], [ 2, "minute" ], [ 5, "minute" ], [ 10, "minute" ], [ 30, "minute" ], [ 1, "hour" ], [ 2, "hour" ], [ 4, "hour" ], [ 8, "hour" ], [ 12, "hour" ], [ 1, "day" ], [ 2, "day" ], [ 3, "day" ], [ 0.25, "month" ], [ 0.5, "month" ], [ 1, "month" ], [ 2, "month" ], [ 3, "month" ], [ 6, "month" ], [ 1, "year" ] ]; var minSize = 0; if( axisOptions.minTickSize != null ) { minSize = typeof axisOptions.tickSize == 'number' ? axisOptions.tickSize: axisOptions.minTickSize[0] * timeUnitSize[axisOptions.minTickSize[1]]; } for( i = 0; i < spec.length - 1; ++i ) { var d = spec[i][0] * timeUnitSize[spec[i][1]] + spec[i + 1][0] * timeUnitSize[spec[i + 1][1]]; if( delta < d / 2 && spec[i][0] * timeUnitSize[spec[i][1]] >= minSize ) { break; } } size = spec[i][0]; unit = spec[i][1]; // special-case the possibility of several years if( unit == "year" ) { magn = Math.pow( 10, Math.floor( Math.log( delta / timeUnitSize.year ) / Math.LN10 ) ); norm = ( delta / timeUnitSize.year ) / magn; if( norm < 1.5 ) size = 1; else if( norm < 3 ) size = 2; else if( norm < 7.5 ) size = 5; else size = 10; size *= magn; } if( axisOptions.tickSize ) { size = axisOptions.tickSize[0]; unit = axisOptions.tickSize[1]; } generator = function( axis ) { var ticks = [], tickSize = axis.tickSize[0], unit = axis.tickSize[1], d = new Date( axis.min ); var step = tickSize * timeUnitSize[unit]; if( unit == 'second' ) d.setUTCSeconds( floorInBase( d.getUTCSeconds(), tickSize ) ); if( unit == 'minute' ) d.setUTCMinutes( floorInBase( d.getUTCMinutes(), tickSize ) ); if( unit == 'hour' ) d.setUTCHours( floorInBase( d.getUTCHours(), tickSize ) ); if( unit == 'month' ) d.setUTCMonth( floorInBase( d.getUTCMonth(), tickSize ) ); if( unit == 'year' ) d.setUTCFullYear( floorInBase( d.getUTCFullYear(), tickSize ) ); // reset smaller components d.setUTCMilliseconds( 0 ); if( step >= timeUnitSize.minute ) d.setUTCSeconds( 0 ); if( step >= timeUnitSize.hour ) d.setUTCMinutes( 0 ); if( step >= timeUnitSize.day ) d.setUTCHours( 0 ); if( step >= timeUnitSize.day * 4 ) d.setUTCDate( 1 ); if( step >= timeUnitSize.year ) d.setUTCMonth( 0 ); var carry = 0, v = Number.NaN, prev; do { prev = v; v = d.getTime(); ticks.push( { v: v, label: axis.tickFormatter( v, axis ) } ); if( unit == 'month' ) { if( tickSize < 1 ) { // a bit complicated - we'll divide the month // up but we need to take care of fractions // so we don't end up in the middle of a day d.setUTCDate( 1 ); var start = d.getTime(); d.setUTCMonth( d.getUTCMonth() + 1 ); var end = d.getTime(); d.setTime( v + carry * timeUnitSize.hour + ( end - start ) * tickSize ); carry = d.getUTCHours(); d.setUTCHours( 0 ); } else { d.setUTCMonth( d.getUTCMonth() + tickSize ); } } else if( unit == 'year' ) { d.setUTCFullYear( d.getUTCFullYear() + tickSize ); } else { d.setTime( v + step ); } } while( v < axis.max && v != prev ); return ticks; }; formatter = function( v, axis ) { var d = new Date( v ); // first check global format if( axisOptions.timeformat ) { return formatDate( d, axisOptions.timeformat, axisOptions.monthNames ); } var t = axis.tickSize[0] * timeUnitSize[axis.tickSize[1]]; var span = axis.max - axis.min; if( t < timeUnitSize.minute ) { fmt = "%h:%M:%S"; } else if( t < timeUnitSize.day ) { if( span < 2 * timeUnitSize.day ) fmt = "%h:%M"; else fmt = "%b %d %h:%M"; } else if( t < timeUnitSize.month ) { fmt = "%b %d"; } else if( t < timeUnitSize.year ) { if( span < timeUnitSize.year ) fmt = "%b"; else fmt = "%b %y"; } else { fmt = "%y"; } return formatDate( d, fmt, axisOptions.monthNames ); }; } else { // pretty rounding of base-10 numbers var maxDec = axisOptions.tickDecimals; var dec = -Math.floor( Math.log( delta ) / Math.LN10 ); if( maxDec && dec > maxDec ) dec = maxDec; magn = Math.pow( 10, -dec ); norm = delta / magn; // norm is between 1.0 and 10.0 if( norm < 1.5 ) { size = 1; } else if( norm < 3 ) { size = 2; // special case for 2.5, requires an extra decimal if( norm > 2.25 && ( !maxDec || dec + 1 <= maxDec ) ) { size = 2.5; ++dec; } } else if( norm < 7.5 ) { size = 5; } else { size = 10; } size *= magn; if( axisOptions.minTickSize && size < axisOptions.minTickSize ) { size = axisOptions.minTickSize; } if( axisOptions.tickSize ) { size = axisOptions.tickSize; } axis.tickDecimals = Math.max( 0, ( maxDec ) ? maxDec : dec ); generator = function( axis ) { var ticks = []; var start = floorInBase( axis.min, axis.tickSize ); // then spew out all possible ticks var i = 0, v = Number.NaN, prev; do { prev = v; v = start + i * axis.tickSize; ticks.push( { v: v, label: axis.tickFormatter( v, axis ) } ); ++i; } while( v < axis.max && v != prev ); return ticks; }; formatter = function( v, axis ) { return v.toFixed( axis.tickDecimals ); }; } axis.tickSize = unit ? [size, unit] : size; axis.tickGenerator = generator; if( $.isFunction( axisOptions.tickFormatter ) ) { axis.tickFormatter = function( v, axis ) { return '' + axisOptions.tickFormatter( v, axis ); }; } else { axis.tickFormatter = formatter; } if( axisOptions.labelWidth ) axis.labelWidth = axisOptions.labelWidth; if( axisOptions.labelHeight ) axis.labelHeight = axisOptions.labelHeight; } function extendXRangeIfNeededByBar() { if( !options.xaxis.max ) { // great, we're autoscaling, check if we might need a bump var newmax = xaxis.max; for( var i = 0; i < series.length; ++i ) { if( series[i].bars.show && series[i].bars.barWidth + xaxis.datamax > newmax ) { newmax = xaxis.datamax + series[i].bars.barWidth; } } xaxis.max = newmax; } } function setTicks( axis, axisOptions ) { axis.ticks = []; if( !axisOptions.ticks ) { axis.ticks = axis.tickGenerator( axis ); } else if( typeof axisOptions.ticks == 'number' ) { if( axisOptions.ticks > 0 ) axis.ticks = axis.tickGenerator( axis ); } else if( axisOptions.ticks ) { var ticks = axisOptions.ticks; if( $.isFunction( ticks ) ) { // generate the ticks ticks = ticks( { min: axis.min, max: axis.max } ); } // clean up the user-supplied ticks, copy them over var i, v; for( i = 0; i < ticks.length; ++i ) { var label = null; var t = ticks[i]; if( typeof t == 'object' ) { v = t[0]; if( t.length > 1 ) label = t[1]; } else { v = t; } if( !label ) label = axis.tickFormatter( v, axis ); axis.ticks[i] = { v: v, label: label }; } } if( axisOptions.autoscaleMargin && axis.ticks.length > 0 ) { // snap to ticks if( !axisOptions.min ) { axis.min = Math.min( axis.min, axis.ticks[0].v ); } if( !axisOptions.max && axis.ticks.length > 1 ) { axis.max = Math.min( axis.max, axis.ticks[axis.ticks.length - 1].v ); } } } function setSpacing() { var i, l, labels = []; if( !yaxis.labelWidth || !yaxis.labelHeight ) { // calculate y label dimensions for( i = 0; i < yaxis.ticks.length; ++i ) { l = yaxis.ticks[i].label; if( l ) labels.push( '
' + l + '
' ); } if( labels.length > 0 ) { var dummyDiv = $( '
' + labels.join('') + '
' ).appendTo( target ); if( !yaxis.labelWidth ) yaxis.labelWidth = dummyDiv.width(); if( !yaxis.labelHeight ) yaxis.labelHeight = dummyDiv.find('div').height(); dummyDiv.remove(); } if( !yaxis.labelWidth ) yaxis.labelWidth = 0; if( !yaxis.labelHeight ) yaxis.labelHeight = 0; } var maxOutset = options.grid.borderWidth / 2; if( options.points.show ) { maxOutset = Math.max( maxOutset, options.points.radius + options.points.lineWidth / 2 ); } for( i = 0; i < series.length; ++i ) { if( series[i].points.show ) { maxOutset = Math.max( maxOutset, series[i].points.radius + series[i].points.lineWidth / 2 ); } } plotOffset.left = plotOffset.right = plotOffset.top = plotOffset.bottom = maxOutset; if( yaxis.labelWidth > 0 && options.xaxis.showLabels ) { plotOffset.left += yaxis.labelWidth + options.grid.labelMargin; } plotWidth = canvasWidth - plotOffset.left - plotOffset.right - RIGHT_SIDE_BUFFER; // set width for labels; to avoid measuring the widths of // the labels, we construct fixed-size boxes and put the // labels inside them, the fixed-size boxes are easy to // mid-align if( !xaxis.labelWidth ) xaxis.labelWidth = plotWidth / 6; if( !xaxis.labelHeight ) { // measure x label heights labels = []; for( i = 0; i < xaxis.ticks.length; ++i ) { l = xaxis.ticks[i].label; if( l ) labels.push( '' + l + '' ); } xaxis.labelHeight = 0; if( labels.length > 0 ) { var dummyDiv = $( '
' + labels.join('') + '
' ).appendTo( target ); xaxis.labelHeight = dummyDiv.height(); dummyDiv.remove(); } } if( xaxis.labelHeight > 0 && options.yaxis.showLabels ) { plotOffset.bottom += xaxis.labelHeight + options.grid.labelMargin; } // add a bit of extra buffer on the bottom of the graph to account // for the axis label, if there is one if( options.xaxis.label ) plotOffset.bottom += BOTTOM_SIDE_BUFFER; plotHeight = canvasHeight - plotOffset.bottom - BOTTOM_SIDE_BUFFER - plotOffset.top; hozScale = plotWidth / ( xaxis.max - xaxis.min ); vertScale = plotHeight / ( yaxis.max - yaxis.min ); } function draw() { drawGrid(); drawMarkers(); for( var i = 0; i < series.length; i++ ) { drawSeries( series[i] ); } } function tHoz( x ) { return ( x - xaxis.min ) * hozScale; } function tVert( y ) { return plotHeight - ( y - yaxis.min ) * vertScale; } function drawGrid() { var i; ctx.save(); ctx.clearRect( 0, 0, canvasWidth, canvasHeight ); ctx.translate( plotOffset.left, plotOffset.top ); // draw background, if any if( options.grid.backgroundColor ) { ctx.fillStyle = options.grid.backgroundColor; ctx.fillRect( 0, 0, plotWidth, plotHeight ); } // draw colored areas if( options.grid.coloredAreas ) { var areas = options.grid.coloredAreas; if( $.isFunction( areas ) ) { areas = areas( { xmin: xaxis.min, xmax: xaxis.max, ymin: yaxis.min, ymax: yaxis.max } ); } for( i = 0; i < areas.length; ++i ) { var a = areas[i]; // clip if( !a.x1 || a.x1 < xaxis.min ) a.x1 = xaxis.min; if( !a.x2 || a.x2 > xaxis.max ) a.x2 = xaxis.max; if( !a.y1 || a.y1 < yaxis.min ) a.y1 = yaxis.min; if( !a.y2 || a.y2 > yaxis.max ) a.y2 = yaxis.max; var tmp; if( a.x1 > a.x2 ) { tmp = a.x1; a.x1 = a.x2; a.x2 = tmp; } if (a.y1 > a.y2) { tmp = a.y1; a.y1 = a.y2; a.y2 = tmp; } if( a.x1 >= xaxis.max || a.x2 <= xaxis.min || a.x1 == a.x2 || a.y1 >= yaxis.max || a.y2 <= yaxis.min || a.y1 == a.y2 ) { continue; } ctx.fillStyle = a.color || options.grid.coloredAreasColor; ctx.fillRect( Math.floor( tHoz( a.x1 ) ), Math.floor( tVert( a.y2 ) ), Math.floor( tHoz( a.x2 ) - tHoz( a.x1 ) ), Math.floor( tVert( a.y1 ) - tVert( a.y2 ) ) ); } } // draw the inner grid ctx.lineWidth = options.grid.tickWidth; ctx.strokeStyle = options.grid.tickColor; ctx.beginPath(); var v; if( options.grid.showLines == 'x' || options.grid.showLines == 'both' ) { for( i = 0; i < xaxis.ticks.length; ++i ) { v = xaxis.ticks[i].v; // skip those lying on the axes if( v <= xaxis.min || v >= xaxis.max ) continue; ctx.moveTo( Math.floor( tHoz( v ) ) + ctx.lineWidth / 2, 0 ); ctx.lineTo( Math.floor( tHoz( v ) ) + ctx.lineWidth / 2, plotHeight ); } } if( options.grid.showLines == 'y' || options.grid.showLines == 'both' ) { for( i = 0; i < yaxis.ticks.length; ++i ) { v = yaxis.ticks[i].v; if( v <= yaxis.min || v >= yaxis.max ) continue; ctx.moveTo( 0, Math.floor( tVert( v ) ) + ctx.lineWidth / 2 ); ctx.lineTo( plotWidth, Math.floor( tVert( v ) ) + ctx.lineWidth / 2 ); } } ctx.stroke(); if( options.grid.showBorder && options.grid.borderWidth ) { // draw border ctx.lineWidth = options.grid.borderWidth; ctx.strokeStyle = options.grid.color; ctx.lineJoin = 'round'; ctx.strokeRect( 0, 0, plotWidth, plotHeight ); ctx.restore(); } } function insertTickLabels() { target.find('.tickLabels').remove(); var i, tick; var html = '
'; // do the x-axis if( options.xaxis.showLabels ) { for( i = 0; i < xaxis.ticks.length; ++i ) { tick = xaxis.ticks[i]; if( !tick.label || tick.v < xaxis.min || tick.v > xaxis.max ) continue; html += '
' + tick.label + "
"; } } // do the y-axis if( options.yaxis.showLabels ) { for( i = 0; i < yaxis.ticks.length; ++i ) { tick = yaxis.ticks[i]; if( !tick.label || tick.v < yaxis.min || tick.v > yaxis.max ) continue; html += '
' + tick.label + "
"; } } html += '
'; target.append( html ); } function insertAxisLabels() { if( options.xaxis.label ) { yLocation = plotOffset.top + plotHeight + ( xaxis.labelHeight * 1.5 ); xLocation = plotOffset.left; target.find('#xaxislabel').remove(); target.append( "
" + options.xaxis.label + "
" ); } if( options.yaxis.label ) { var element; if( $.browser.msie ) { element = "" + options.yaxis.label + ""; } else { // we'll use svg instead var element = document.createElement( 'object' ); element.setAttribute( 'type', 'image/svg+xml' ); xAxisHeight = $('#xaxislabel').height(); string = ''; string += '' + options.yaxis.label + ''; element.setAttribute( 'data', 'data:image/svg+xml,' + string ); } xLocation = plotOffset.left - ( yaxis.labelWidth * 1.5 ) - options.grid.labelFontSize; yLocation = plotOffset.top; var yAxisLabel = $("
"); yAxisLabel.append( element ); target.find('#yaxislabel').remove().end().append( yAxisLabel ); } } function drawSeries( series ) { if( series.lines.show || ( !series.bars.show && !series.points.show && !series.deltas.show ) ) { drawSeriesLines( series ); } if( series.bars.show ) drawSeriesBars( series ); if( series.points.show ) drawSeriesPoints( series ); if( series.deltas.show ) drawSeriesDeltas( series ); } function drawSeriesLines( series ) { function plotLine( data, offset ) { var prev = cur = drawx = drawy = null; ctx.beginPath(); for( var i = 0; i < data.length; ++i ) { prev = cur; cur = data[i]; if( !prev || !cur ) continue; var x1 = prev.x, y1 = prev.y, x2 = cur.x, y2 = cur.y; // clip with ymin if( y1 <= y2 && y1 < yaxis.min ) { if( y2 < yaxis.min ) continue; // line segment is outside // compute new intersection point x1 = ( yaxis.min - y1 ) / ( y2 - y1 ) * ( x2 - x1 ) + x1; y1 = yaxis.min; } else if( y2 <= y1 && y2 < yaxis.min ) { if( y1 < yaxis.min ) continue; x2 = ( yaxis.min - y1 ) / ( y2 - y1 ) * ( x2 - x1 ) + x1; y2 = yaxis.min; } // clip with ymax if( y1 >= y2 && y1 > yaxis.max ) { if( y2 > yaxis.max ) continue; x1 = ( yaxis.max - y1 ) / ( y2 - y1 ) * ( x2 - x1 ) + x1; y1 = yaxis.max; } else if( y2 >= y1 && y2 > yaxis.max ) { if( y1 > yaxis.max ) continue; x2 = ( yaxis.max - y1 ) / ( y2 - y1 ) * ( x2 - x1 ) + x1; y2 = yaxis.max; } // clip with xmin if( x1 <= x2 && x1 < xaxis.min ) { if( x2 < xaxis.min ) continue; y1 = ( xaxis.min - x1 ) / ( x2 - x1 ) * ( y2 - y1 ) + y1; x1 = xaxis.min; } else if( x2 <= x1 && x2 < xaxis.min ) { if( x1 < xaxis.min ) continue; y2 = ( xaxis.min - x1 ) / ( x2 - x1 ) * ( y2 - y1 ) + y1; x2 = xaxis.min; } // clip with xmax if( x1 >= x2 && x1 > xaxis.max ) { if( x2 > xaxis.max ) continue; y1 = ( xaxis.max - x1 ) / ( x2 - x1 ) * ( y2 - y1 ) + y1; x1 = xaxis.max; } else if( x2 >= x1 && x2 > xaxis.max ) { if( x1 > xaxis.max ) continue; y2 = ( xaxis.max - x1 ) / ( x2 - x1 ) * ( y2 - y1 ) + y1; x2 = xaxis.max; } if( drawx != tHoz( x1 ) || drawy != tVert( y1 ) + offset ) { ctx.moveTo( tHoz( x1 ), tVert( y1 ) + offset ); } drawx = tHoz( x2 ); drawy = tVert( y2 ) + offset; ctx.lineTo( drawx, drawy ); } ctx.stroke(); } function plotLineArea( data ) { var prev = cur = null; var bottom = Math.min( Math.max( 0, yaxis.min ), yaxis.max ); var top, lastX = 0; var areaOpen = false; for( var i = 0; i < data.length; ++i ) { prev = cur; cur = data[i]; if( areaOpen && prev && !cur ) { // close area ctx.lineTo( tHoz( lastX ), tVert( bottom ) ); ctx.fill(); areaOpen = false; continue; } if( !prev || !cur ) continue; var x1 = prev.x, y1 = prev.y, x2 = cur.x, y2 = cur.y; // clip with xmin if( x1 <= x2 && x1 < xaxis.min ) { if( x2 < xaxis.min ) continue; y1 = ( xaxis.min - x1 ) / ( x2 - x1 ) * ( y2 - y1 ) + y1; x1 = xaxis.min; } else if( x2 <= x1 && x2 < xaxis.min ) { if( x1 < xaxis.min ) continue; y2 = ( xaxis.min - x1 ) / (x2 - x1) * (y2 - y1) + y1; x2 = xaxis.min; } // clip with xmax if( x1 >= x2 && x1 > xaxis.max ) { if( x2 > xaxis.max ) continue; y1 = ( xaxis.max - x1 ) / ( x2 - x1 ) * ( y2 - y1 ) + y1; x1 = xaxis.max; } else if( x2 >= x1 && x2 > xaxis.max ) { if( x1 > xaxis.max ) continue; y2 = ( xaxis.max - x1 ) / ( x2 - x1 ) * ( y2 - y1 ) + y1; x2 = xaxis.max; } if( !areaOpen ) { // open area ctx.beginPath(); ctx.moveTo( tHoz( x1 ), tVert( bottom ) ); areaOpen = true; } // now first check the case where both is outside if( y1 >= yaxis.max && y2 >= yaxis.max ) { ctx.lineTo( tHoz( x1 ), tVert( yaxis.max ) ); ctx.lineTo( tHoz( x2 ), tVert( yaxis.max ) ); continue; } else if( y1 <= yaxis.min && y2 <= yaxis.min ) { ctx.lineTo( tHoz( x1 ), tVert( yaxis.min ) ); ctx.lineTo( tHoz( x2 ), tVert( yaxis.min ) ); continue; } // else it's a bit more complicated, there might // be two rectangles and two triangles we need to fill // in; to find these keep track of the current x values var x1old = x1, x2old = x2; // and clip the y values, without shortcutting // clip with ymin if( y1 <= y2 && y1 < yaxis.min && y2 >= yaxis.min ) { x1 = ( yaxis.min - y1 ) / ( y2 - y1 ) * ( x2 - x1 ) + x1; y1 = yaxis.min; } else if( y2 <= y1 && y2 < yaxis.min && y1 >= yaxis.min ) { x2 = ( yaxis.min - y1 ) / ( y2 - y1 ) * ( x2 - x1 ) + x1; y2 = yaxis.min; } // clip with ymax if( y1 >= y2 && y1 > yaxis.max && y2 <= yaxis.max ) { x1 = ( yaxis.max - y1 ) / ( y2 - y1 ) * ( x2 - x1 ) + x1; y1 = yaxis.max; } else if( y2 >= y1 && y2 > yaxis.max && y1 <= yaxis.max ) { x2 = ( yaxis.max - y1 ) / ( y2 - y1 ) * ( x2 - x1 ) + x1; y2 = yaxis.max; } // if the x value was changed we got a rectangle to fill if( x1 != x1old ) { top = y1 <= yaxis.min ? yaxis.min : yaxis.max; ctx.lineTo( tHoz( x1old ), tVert( top ) ); ctx.lineTo( tHoz( x1 ), tVert( top ) ); } // fill the triangles ctx.lineTo( tHoz( x1 ), tVert( y1 ) ); ctx.lineTo( tHoz( x2 ), tVert( y2 ) ); // fill the other rectangle if it's there if( x2 != x2old ) { top = y2 <= yaxis.min ? yaxis.min : yaxis.max; ctx.lineTo( tHoz( x2old ), tVert( top ) ); ctx.lineTo( tHoz( x2 ), tVert( top ) ); } lastX = Math.max( x2, x2old ); } if( areaOpen ) { ctx.lineTo( tHoz( lastX ), tVert( bottom ) ); ctx.fill(); } } ctx.save(); ctx.translate( plotOffset.left, plotOffset.top ); ctx.lineJoin = 'round'; var lw = series.lines.lineWidth; var sw = series.shadowSize; // FIXME: consider another form of shadow when filling is turned on if( sw > 0 ) { // draw shadow in two steps ctx.lineWidth = sw / 2; ctx.strokeStyle = 'rgba(0,0,0,0.1)'; plotLine( series.data, lw / 2 + sw / 2 + ctx.lineWidth / 2 ); ctx.lineWidth = sw / 2; ctx.strokeStyle = 'rgba(0,0,0,0.2)'; plotLine( series.data, lw / 2 + ctx.lineWidth / 2 ); } ctx.lineWidth = lw; ctx.strokeStyle = series.color; setFillStyle( series.lines, series.color ); if( series.lines.fill ) plotLineArea( series.data, 0 ); plotLine( series.data, 0 ); ctx.restore(); } function drawSeriesPoints( series ) { function plotPoints( data, radius, fill ) { for( var i = 0; i < data.length; ++i ) { if( !data[i] ) continue; var x = data[i].x, y = data[i].y; if( x < xaxis.min || x > xaxis.max || y < yaxis.min || y > yaxis.max ) { continue; } ctx.beginPath(); ctx.arc( tHoz(x), tVert( y ), radius, 0, 2 * Math.PI, true ); if( fill ) ctx.fill(); ctx.stroke(); } } function plotPointShadows( data, offset, radius ) { for( var i = 0; i < data.length; ++i ) { if( !data[i] ) continue; var x = data[i].x, y = data[i].y; if( x < xaxis.min || x > xaxis.max || y < yaxis.min || y > yaxis.max ) { continue; } ctx.beginPath(); ctx.arc( tHoz( x ), tVert( y ) + offset, radius, 0, Math.PI, false ); ctx.stroke(); } } ctx.save(); ctx.translate( plotOffset.left, plotOffset.top ); var lw = series.lines.lineWidth; var sw = series.shadowSize; if( sw > 0 ) { // draw shadow in two steps ctx.lineWidth = sw / 2; ctx.strokeStyle = 'rgba(0,0,0,0.1)'; plotPointShadows( series.data, sw / 2 + ctx.lineWidth / 2, series.points.radius ); ctx.lineWidth = sw / 2; ctx.strokeStyle = 'rgba(0,0,0,0.2)'; plotPointShadows( series.data, ctx.lineWidth / 2, series.points.radius ); } ctx.lineWidth = series.points.lineWidth; ctx.strokeStyle = series.color; setFillStyle( series.points, series.color ); plotPoints( series.data, series.points.radius, series.points.fill ); ctx.restore(); } function drawSeriesDeltas( series ) { function plotPoints( data, radius, fill ) { for( var i = 0; i < data.length; ++i ) { if( !data[i] ) continue; var x = data[i].x, y = data[i].y, d = data[i].delta; if( x < xaxis.min || x > xaxis.max || y < yaxis.min || y > yaxis.max || d < yaxis.min || d > yaxis.max ) { continue; } ctx.beginPath(); ctx.arc( tHoz( x ), tVert( y ), radius, 0, 2 * Math.PI, true ); if( fill ) ctx.fill(); ctx.stroke(); } } function plotDeltas( data, settings ) { for( var i = 0; i < data.length; ++i ) { if( !data[i] ) { continue; } var x = data[i].x, y = data[i].y, d = data[i].delta; if( x < xaxis.min || x > xaxis.max || y < yaxis.min || y > yaxis.max || d < yaxis.min || d > yaxis.max ) { continue; } if( y < d ) ctx.strokeStyle = settings.color.below; else if( y > d ) ctx.strokeStyle = settings.color.above; else ctx.strokeStyle = settings.color.equal; ctx.beginPath(); ctx.moveTo( tHoz( x ), tVert( y ) ); ctx.lineTo( tHoz( x ), tVert( d ) ); ctx.stroke(); // draw the markers for the deltas (horizontal line) var markerLeft = tHoz( x ) - ( ctx.lineWidth * settings.markerWidth ); var markerRight = tHoz( x ) + ( ctx.lineWidth * settings.markerWidth ); ctx.beginPath(); ctx.moveTo( markerLeft, tVert( d ) ); ctx.lineTo( markerRight, tVert( d ) ); ctx.stroke(); } } function plotPointShadows( data, offset, radius ) { for( var i = 0; i < data.length; ++i ) { if( !data[i] ) continue; var x = data[i].x, y = data[i].y, d = data[i].delta; if( x < xaxis.min || x > xaxis.max || y < yaxis.min || y > yaxis.max || d < yaxis.min || d > yaxis.max ) { continue; } ctx.beginPath(); ctx.arc( tHoz( x ), tVert( y ) + offset, radius, 0, Math.PI, false ); ctx.stroke(); } } ctx.save(); ctx.translate( plotOffset.left, plotOffset.top ); var lw = series.lines.lineWidth; var sw = series.shadowSize; if( sw > 0 ) { // draw shadow in two steps ctx.lineWidth = sw / 2; ctx.strokeStyle = 'rgba(0,0,0,0.1)'; plotPointShadows( series.data, sw / 2 + ctx.lineWidth / 2, series.points.radius ); ctx.lineWidth = sw / 2; ctx.strokeStyle = 'rgba(0,0,0,0.2)'; plotPointShadows( series.data, ctx.lineWidth / 2, series.points.radius ); } ctx.lineWidth = series.points.lineWidth; // draw the delta lines and markers plotDeltas( series.data, series.deltas ); // draw the actual datapoints ctx.strokeStyle = series.color; setFillStyle( series.points, series.color ); plotPoints( series.data, series.points.radius, series.points.fill ); ctx.restore(); } function drawSeriesBars( series ) { function plotBars( data, barWidth, offset, fill ) { for( var i = 0; i < data.length; i++ ) { if( !data[i] ) continue; var x = data[i].x, y = data[i].y, drawLeft = true, drawTop = true, drawRight = true; // determine the co-ordinates of the bar, account for negative bars having // flipped top/bottom and draw/don't draw accordingly var halfBar = barWidth / 2; var left = x - halfBar, right = x + halfBar, bottom = ( y < 0 ? y : 0 ), top = ( y < 0 ? 0 : y ); if( right < xaxis.min || left > xaxis.max || top < yaxis.min || bottom > yaxis.max ) { continue; } // clip if( left < xaxis.min ) { left = xaxis.min; drawLeft = false; } if ( right > xaxis.max ) { right = xaxis.max; drawRight = false; } if( bottom < yaxis.min ) { bottom = yaxis.min; } if (top > yaxis.max) { top = yaxis.max; drawTop = false; } // fill the bar if( fill ) { ctx.beginPath(); ctx.moveTo( tHoz( left ), tVert( bottom) + offset ); ctx.lineTo( tHoz( left ), tVert( top) + offset ); ctx.lineTo( tHoz( right ), tVert( top) + offset ); ctx.lineTo( tHoz( right ), tVert( bottom) + offset ); ctx.fill(); } // draw outline if( drawLeft || drawRight || drawTop ) { ctx.beginPath(); ctx.moveTo( tHoz( left ), tVert( bottom ) + offset ); if( drawLeft ) ctx.lineTo( tHoz( left ), tVert( top) + offset ); else ctx.moveTo( tHoz( left ), tVert( top) + offset ); if( drawTop ) ctx.lineTo( tHoz( right ), tVert( top ) + offset ); else ctx.moveTo( tHoz( right ), tVert( top ) + offset ); if( drawRight ) ctx.lineTo( tHoz( right ), tVert( bottom ) + offset ); else ctx.moveTo( tHoz( right ), tVert( bottom ) + offset ); ctx.stroke(); } } } ctx.save(); ctx.translate( plotOffset.left, plotOffset.top ); ctx.lineJoin = 'round'; var bw = series.bars.barWidth; var lw = Math.min( series.bars.lineWidth, bw ); ctx.lineWidth = lw; ctx.strokeStyle = series.color; setFillStyle( series.bars, series.color ); plotBars( series.data, bw, 0, series.bars.fill ); ctx.restore(); } function setFillStyle( obj, seriesColor ) { var opacity = obj.fillOpacity; if( obj.fill ) { if( obj.fillColor ) { ctx.fillStyle = obj.fillColor; } else { var c = parseColor( seriesColor ); c.a = typeof fill == 'number' ? obj.fill : ( opacity ? opacity : 0.4 ); c.normalize(); ctx.fillStyle = c.toString(); } } } function drawMarkers() { if( !options.grid.markers.length ) return; for( var i = 0; i < options.grid.markers.length; i++ ) { marker = options.grid.markers[i]; if( marker.value < yaxis.max && marker.value > yaxis.min ) { ctx.lineWidth = marker.width; ctx.strokeStyle = marker.color; ctx.beginPath(); if( marker.axis == 'x' ) { ctx.moveTo( tHoz( xaxis.min ) + plotOffset.left, tVert( marker.value ) + plotOffset.top ); ctx.lineTo( tHoz( xaxis.max ) + plotOffset.left, tVert( marker.value ) + plotOffset.top ); } else if( marker.axis == 'y' ) { ctx.moveTo( tHoz( marker.value ) + plotOffset.left, tVert( yaxis.min ) + plotOffset.top ); ctx.lineTo( tHoz( marker.value ) + plotOffset.left, tVert( yaxis.max ) + plotOffset.top ); } ctx.stroke(); } } } function insertLegend() { // remove legends from the appropriate container if( options.legend.container ) { options.legend.container.find('table.legend_table').remove(); } else { target.find('.legend').remove(); } if( !options.legend.show ) { return; } var fragments = []; var rowStarted = false; for( i = 0; i < series.length; ++i ) { if( !series[i].label ) continue; if( i % options.legend.noColumns == 0 ) { if( rowStarted ) fragments.push( '' ); fragments.push( '' ); rowStarted = true; } var label = series[i].label; if( options.legend.labelFormatter ) label = options.legend.labelFormatter( label ); fragments.push( '
' + '' + label + '' ); } if( rowStarted ) fragments.push( '' ); if( fragments.length > 0 ) { var table = '' + fragments.join('') + '
'; if( options.legend.container ) { options.legend.container.append( table ); } else { var pos = ''; var p = options.legend.position, m = options.legend.margin; if( p.charAt( 0 ) == 'n' ) { pos += 'top:' + ( m + plotOffset.top ) + 'px;'; } else if( p.charAt( 0 ) == 's' ) { pos += 'bottom:' + ( m + plotOffset.bottom + BOTTOM_SIDE_BUFFER ) + 'px;'; } if( p.charAt( 1 ) == 'e' ) { pos += 'right:' + ( m + plotOffset.right + RIGHT_SIDE_BUFFER ) + 'px;'; } else if( p.charAt( 1 ) == 'w' ) { pos += 'left:' + ( m + plotOffset.left ) + 'px;'; } var legend = $('
' + table.replace( 'style="', 'style="position:absolute;' + pos +';' ) + '
').appendTo( target ); if( options.legend.backgroundOpacity != 0.0 ) { // put in the transparent background // separately to avoid blended labels and // label boxes var c = options.legend.backgroundColor; if( !c ) { tmp = options.grid.backgroundColor ? options.grid.backgroundColor : extractColor( legend ); c = parseColor( tmp ).adjust( null, null, null, 1 ).toString(); } var div = legend.children(); $('
') .prependTo( legend ) .css( 'opacity', options.legend.backgroundOpacity ); } } } } var lastMousePos = { pageX: null, pageY: null }, selection = { first: { x: -1, y: -1 }, second: { x: -1, y: -1 } }, prevSelection = null, selectionInterval = null, ignoreClick = false; // Returns the data item the mouse is over, or null if none is found function findSelectedItem( mouseX, mouseY ) { // How close do we need to be to an item in order to select it? // The clickCatchingArea parameter is the radius of the circle, in pixels. var lowestDistance = Math.pow( options.grid.mouseCatchingArea, 2 ); selectedItem = null; for( var i = 0; i < series.length; ++i ) { var data = series[i].data; if( options.sortData && data.length > 1 ) { var half = tHoz( data[( data.length / 2 ).toFixed(0)].x ).toFixed( 0 ); if( mouseX < half ) { start = 0; end = ( data.length / 2 ).toFixed( 0 ) + 5; } else { start = ( data.length / 2 ).toFixed( 0 ) - 5; end = data.length; } } else { // either we haven't sorted the data (and so we can't split it for // searching) or there's only 1 data point, so it doesn't matter start = 0; end = data.length; } for( var j = start; j < end; ++j ) { if( !data[j] ) continue; // We have to calculate distances in pixels, not in data units, because // the scale of the axes may be different var x = data[j].x, y = data[j].y; xDistance = Math.abs( tHoz( x ) - mouseX ); yDistance = Math.abs(tVert(y)-mouseY); if( xDistance > options.grid.mouseCatchingArea ) continue; if( yDistance > options.grid.mouseCatchingArea ) continue; sqrDistance = Math.pow( xDistance, 2 ) + Math.pow( yDistance, 2 ); if( sqrDistance < lowestDistance ) { selectedItem = data[j]; selectedItem._data = series[i]; lowestDistance = sqrDistance; } } } return selectedItem; } function onMouseMove( ev ) { // FIXME: temp. work-around until jQuery bug 1871 is fixed var e = ev || window.event; if( !e.pageX && e.clientX ) { var de = document.documentElement, b = document.body; lastMousePos.pageX = e.clientX + ( de && de.scrollLeft || b.scrollLeft || 0 ); lastMousePos.pageY = e.clientY + ( de && de.scrollTop || b.scrollTop || 0 ); } else { lastMousePos.pageX = e.pageX; lastMousePos.pageY = e.pageY; } if( options.grid.hoverable ) { var offset = eventHolder.offset(); result = { raw: { x: lastMousePos.pageX - offset.left - plotOffset.left, y: lastMousePos.pageY - offset.top - plotOffset.top } }; result.selected = findSelectedItem( result.raw.x, result.raw.y ); // display the tooltip/hint if requested if( !$.browser.msie && result.selected && result.selected._data.hints.show ) { showHintDiv( result.selected ); } if( !result.selected ) cleanup(); target.trigger( 'plotmousemove', [ result ] ); } } function onMouseDown( e ) { if( e.which != 1 ) return; // left click // cancel out any text selections document.body.focus(); // prevent text selection and drag in old-school browsers if( document.onselectstart !== undefined && !workarounds.onselectstart ) { workarounds.onselectstart = document.onselectstart; document.onselectstart = function() { return false; }; } if( document.ondrag !== undefined && !workarounds.ondrag ) { workarounds.ondrag = document.ondrag; document.ondrag = function() { return false; }; } setSelectionPos( selection.first, e ); clearInterval( selectionInterval ); lastMousePos.pageX = null; selectionInterval = setInterval( updateSelectionOnMouseMove, 200 ); $(document).one( 'mouseup', onSelectionMouseUp ); } function onClick( e ) { if( ignoreClick ) { ignoreClick = false; return; } var offset = eventHolder.offset(); var canvasX = e.pageX - offset.left - plotOffset.left; var canvasY = e.pageY - offset.top - plotOffset.top; var result = { raw: { x: xaxis.min + canvasX / hozScale, y: yaxis.max - canvasY / vertScale } }; result.selected = findSelectedItem( canvasX, canvasY ); target.trigger( 'plotclick', [ result ] ); } function triggerSelectedEvent() { var x1, x2, y1, y2; if( selection.first.x <= selection.second.x ) { x1 = selection.first.x; x2 = selection.second.x; } else { x1 = selection.second.x; x2 = selection.first.x; } if( selection.first.y >= selection.second.y ) { y1 = selection.first.y; y2 = selection.second.y; } else { y1 = selection.second.y; y2 = selection.first.y; } x1 = xaxis.min + x1 / hozScale; x2 = xaxis.min + x2 / hozScale; y1 = yaxis.max - y1 / vertScale; y2 = yaxis.max - y2 / vertScale; target.trigger( 'plotselected', [ { x1: x1, y1: y1, x2: x2, y2: y2 } ] ); } function onSelectionMouseUp( e ) { if( document.onselectstart !== undefined ) document.onselectstart = workarounds.onselectstart; if( document.ondrag !== undefined ) document.ondrag = workarounds.ondrag; if( selectionInterval ) { clearInterval( selectionInterval ); selectionInterval = null; } setSelectionPos( selection.second, e ); clearSelection(); if( !selectionIsSane() || e.which != 1 ) return false; drawSelection(); triggerSelectedEvent(); ignoreClick = true; return false; } function setSelectionPos( pos, e ) { var offset = $(overlay).offset(); if( options.selection.mode == 'y' ) { pos.x = ( pos == selection.first ) ? 0 : plotWidth; } else { pos.x = e.pageX - offset.left - plotOffset.left; pos.x = Math.min( Math.max( 0, pos.x ), plotWidth ); if( options.selection.snapToTicks ) { // find our current location in terms of the xaxis var x = xaxis.min + pos.x / hozScale; // determine if we're moving left or right on the xaxis if( selection.first.x - selection.second.x < 0 || selection.first.x == -1 ) { // to the right idx = pos == selection.first ? -1 : 0 for( var i = 0; i < xaxis.ticks.length; i++ ) { if( x <= xaxis.ticks[i].v ) { pos.x = Math.floor( ( xaxis.ticks[i+idx].v - xaxis.min ) * hozScale); break; } } } else { // to the left idx = pos == selection.first ? 1 : 0 for( var i = xaxis.ticks.length - 1; i >= 0; i-- ) { if( x >= xaxis.ticks[i].v ) { pos.x = Math.floor( ( xaxis.ticks[i+idx].v - xaxis.min ) * hozScale); break; } } } } } if( options.selection.mode == 'x' ) { pos.y = ( pos == selection.first ) ? 0 : plotHeight; } else { pos.y = e.pageY - offset.top - plotOffset.top; pos.y = Math.min( Math.max( 0, pos.y ), plotHeight ); } } function updateSelectionOnMouseMove() { if( !lastMousePos.pageX ) { return; } setSelectionPos( selection.second, lastMousePos ); clearSelection(); if( selectionIsSane() ) { drawSelection(); } } function clearSelection() { if( !prevSelection ) { return; } var x = Math.min( prevSelection.first.x, prevSelection.second.x ), y = Math.min( prevSelection.first.y, prevSelection.second.y ), w = Math.abs( prevSelection.second.x - prevSelection.first.x ), h = Math.abs( prevSelection.second.y - prevSelection.first.y ); octx.clearRect( x + plotOffset.left - octx.lineWidth, y + plotOffset.top - octx.lineWidth, w + octx.lineWidth * 2, h + octx.lineWidth * 2 ); prevSelection = null; } function setSelection( area ) { clearSelection(); if( options.selection.mode == 'x' ) { selection.first.y = 0; selection.second.y = plotHeight; } else { selection.first.y = ( yaxis.max - area.y1 ) * vertScale; selection.second.y = ( yaxis.max - area.y2 ) * vertScale; } if( options.selection.mode == 'y' ) { selection.first.x = 0; selection.second.x = plotWidth; } else { selection.first.x = ( area.x1 - xaxis.min ) * hozScale; selection.second.x = ( area.x2 - xaxis.min ) * hozScale; } drawSelection(); triggerSelectedEvent(); } function highlight( marker ) { // prevent unnecessary work if( marker == lastMarker ) { return; } else { lastMarker = marker; } // draw a marker on the graph over the point that the mouse is hovering over if( marker ) { var color = options.grid.hoverColor ? options.grid.hoverColor : marker._data.color; var fill = options.grid.hoverFill ? options.grid.hoverFill : 'white'; var radius = options.grid.hoverRadius ? options.grid.hoverRadius : marker._data.points.radius; var temp_series = { shadowSize: options.shadowSize, lines: { show: false }, points: $.extend( true, options.points, { fillColor: fill, radius: radius } ), color: color, data: [ { x: marker.x, y: marker.y } ] }; draw(); drawSeriesPoints( temp_series ); } else { draw(); } } function drawSelection() { if( prevSelection && selection.first.x == prevSelection.first.x && selection.first.y == prevSelection.first.y && selection.second.x == prevSelection.second.x && selection.second.y == prevSelection.second.y ) { return; } octx.strokeStyle = parseColor( options.selection.color ).scale( null, null, null, 0.8 ).toString(); octx.lineWidth = 1; ctx.lineJoin = 'round'; octx.fillStyle = parseColor( options.selection.color ).scale( null, null, null, 0.4 ).toString(); prevSelection = { first: { x: selection.first.x, y: selection.first.y }, second: { x: selection.second.x, y: selection.second.y } }; var x = Math.min( selection.first.x, selection.second.x ), y = Math.min( selection.first.y, selection.second.y ), w = Math.abs( selection.second.x - selection.first.x ), h = Math.abs( selection.second.y - selection.first.y ); octx.fillRect( x + plotOffset.left, y + plotOffset.top, w, h ); octx.strokeRect( x + plotOffset.left, y + plotOffset.top, w, h ); } function selectionIsSane() { var minSize = 5; return Math.abs( selection.second.x - selection.first.x ) >= minSize && Math.abs( selection.second.y - selection.first.y ) >= minSize; } function showHintDiv(selected) { var offset = $(overlay).offset(); if( $('.hint-wrapper').length > 0 && $('.hint-wrapper:first').attr( 'name' ) == selected.x + ":" + selected.y ) { var hintDiv = $('div.plot-hint'); var hintBackground = $('div.hint-background'); } else { cleanup(); var fragments = []; var hintWrapper = $('
'); hintWrapper.appendTo( target ); fragments.push( '' ); if( selected._data.hints.showColorBox ) { fragments.push( '
' ); } if( selected._data.hints.showSeriesLabel && selected._data.label ) { var label = selected._data.hints.labelFormatter( selected._data.label ); fragments.push( '' + label + ''); } fragments.push( '' ); fragments.push( '' ); hintDiv = $('') .appendTo(hintWrapper); var table = $('' + fragments.join('') + '
'); hintDiv.append( table ); if( selected._data.hints.backgroundOpacity != 0.0 ) { var c = selected._data.hints.backgroundColor; if( !c ){ tmp = options.grid.backgroundColor ? options.grid.backgroundColor : extractColor( hintDiv ); c = parseColor( tmp ).adjust( null, null, null, 1 ).toString(); } hintBackground = $('') .appendTo( hintWrapper ) .css( 'opacity', selected._data.hints.backgroundOpacity ); } var hintDataContainer = hintDiv.find('.hintData'); $(hintDataContainer).html( selected._data.hints.hintFormatter( selected ) ); } leftEdge = lastMousePos.pageX - offset.left + 15; if( hintDiv.width() + leftEdge > target.width() ) { leftEdge = leftEdge - 30 - hintDiv.width(); } hintDiv.css( { left: leftEdge, top: lastMousePos.pageY - offset.top + 15 } ).show(); hintBackground.css( { left: leftEdge, top: lastMousePos.pageY - offset.top + 15, width: hintDiv.width(), height: hintDiv.height() } ).show(); } function cleanup() { $('.hint-wrapper').remove(); draw(); } function defaultHintFormatter( datapoint ) { hintStr = ''; for( var key in datapoint ) { if( key[0] == '_' ) { continue; } // skip internal members hintStr += "" + key + ": " + datapoint[key] + "
"; } return hintStr; } function defaultLabelFormatter( label ) { return "" + label + ""; } } $.plot = function( target, data, options ) { var plot = new Plot( target, data, options ); /*var t0 = new Date(); var t1 = new Date(); var tstr = "time used (msecs): " + (t1.getTime() - t0.getTime()) if (window.console) console.log(tstr); else alert(tstr);*/ return plot; }; // round to nearby lower multiple of base function floorInBase( n, base ) { return base * Math.floor( n / base ); } // color helpers, inspiration from the jquery color animation // plugin by John Resig function Color( r, g, b, a ) { var rgba = [ 'r', 'g', 'b', 'a' ]; var x = 4; //rgba.length while( -1 < --x ) { this[rgba[x]] = arguments[x] || ( ( x == 3 ) ? 1.0 : 0 ); } this.toString = function() { if( this.a >= 1.0 ) { return "rgb(" + [ this.r, this.g, this.b ].join( ',' ) + ")"; } else { return "rgba(" + [ this.r, this.g, this.b, this.a ].join( ',' ) + ")"; } }; this.scale = function( rf, gf, bf, af ) { x = 4; //rgba.length while( -1 < --x ) { if( arguments[x] ) this[rgba[x]] *= arguments[x]; } return this.normalize(); }; this.adjust = function( rd, gd, bd, ad ) { x = 4; //rgba.length while( -1 < --x ) { if( arguments[x] ) this[rgba[x]] += arguments[x]; } return this.normalize(); }; this.clone = function() { return new Color( this.r, this.b, this.g, this.a ); }; var limit = function( val, minVal, maxVal ) { return Math.max( Math.min( val, maxVal ), minVal ); }; this.normalize = function() { this.r = limit( parseInt( this.r ), 0, 255 ); this.g = limit( parseInt( this.g ), 0, 255 ); this.b = limit( parseInt( this.b ), 0, 255 ); this.a = limit( this.a, 0, 1 ); return this; }; this.normalize(); } var lookupColors = { aqua: [ 0, 255, 255 ], azure: [ 240, 255, 255 ], beige: [ 245, 245, 220 ], black: [ 0, 0, 0 ], blue: [ 0, 0, 255 ], brown: [ 165, 42, 42 ], cyan: [ 0, 255, 255 ], darkblue: [ 0, 0, 139 ], darkcyan: [ 0, 139, 139 ], darkgrey: [ 169, 169, 169 ], darkgreen: [ 0, 100, 0 ], darkkhaki: [ 189, 183, 107 ], darkmagenta: [ 139, 0, 139 ], darkolivegreen: [ 85, 107, 47 ], darkorange: [ 255, 140, 0 ], darkorchid: [ 153, 50, 204 ], darkred: [ 139, 0, 0 ], darksalmon: [ 233, 150, 122 ], darkviolet: [ 148, 0, 211 ], fuchsia: [ 255, 0, 255 ], gold: [ 255, 215, 0 ], green: [ 0, 128, 0 ], indigo: [ 75, 0, 130 ], khaki: [ 240, 230, 140 ], lightblue: [ 173, 216, 230 ], lightcyan: [ 224, 255, 255 ], lightgreen: [ 144, 238, 144 ], lightgrey: [ 211, 211, 211 ], lightpink: [ 255, 182, 193 ], lightyellow: [ 255, 255, 224 ], lime: [ 0, 255, 0 ], magenta: [ 255, 0, 255 ], maroon: [ 128, 0, 0 ], navy: [ 0, 0, 128 ], olive: [ 128, 128, 0 ], orange: [ 255, 165, 0 ], pink: [ 255, 192, 203 ], purple: [ 128, 0, 128 ], violet: [ 128, 0, 128 ], red: [ 255, 0, 0 ], silver: [ 192, 192, 192 ], white: [ 255, 255, 255 ], yellow: [ 255, 255, 0 ] }; function extractColor( element ) { var color, elem = element; do { color = elem.css( 'background-color' ).toLowerCase(); // keep going until we find an element that has color, or // we hit the body if( color != '' && color != 'transparent' ) break; elem = elem.parent(); } while( !$.nodeName( elem.get( 0 ), 'body' ) ); // catch Safari's way of signalling transparent if( color == 'rgba(0, 0, 0, 0)' ) return 'transparent'; return color; } // parse string, returns Color function parseColor( str ) { var result; // Try to lookup the color first before going mad with regexes var name = $.trim( str ).toLowerCase(); if (name == 'transparent') { return new Color( 255, 255, 255, 0 ); } else if( !name.match( /^(rgb|#)/ ) ) { result = lookupColors[name]; return new Color( result[0], result[1], result[2] ); } // Look for rgb(num,num,num) if( result = /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec( str ) ) { return new Color( parseInt( result[1], 10 ), parseInt( result[2], 10 ), parseInt( result[3], 10 ) ); } // Look for rgba(num,num,num,num) if( result = /rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec( str ) ) { return new Color( parseInt( result[1], 10 ), parseInt( result[2], 10 ), parseInt( result[3], 10 ), parseFloat( result[4] ) ); } // Look for rgb(num%,num%,num%) if( result = /rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec( str ) ) { return new Color( parseFloat( result[1] ) * 2.55, parseFloat( result[2] ) * 2.55, parseFloat( result[3] ) * 2.55 ); } // Look for rgba(num%,num%,num%,num) if( result = /rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec( str ) ) { return new Color( parseFloat( result[1] ) * 2.55, parseFloat( result[2]) * 2.55, parseFloat( result[3] ) * 2.55, parseFloat( result[4] ) ); } // Look for #a0b1c2 if( result = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec( str ) ) { return new Color( parseInt( result[1], 16 ), parseInt( result[2], 16 ), parseInt( result[3], 16 ) ); } // Look for #fff if( result = /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec( str ) ) { return new Color( parseInt( result[1] + result[1], 16 ), parseInt( result[2] + result[2], 16 ), parseInt( result[3] + result[3], 16 ) ); } } } )( jQuery );