Added: ofbiz/trunk/framework/images/webapp/images/flotr/flotr.debug-0.2.0-alpha.js URL: http://svn.apache.org/viewvc/ofbiz/trunk/framework/images/webapp/images/flotr/flotr.debug-0.2.0-alpha.js?rev=945045&view=auto ============================================================================== --- ofbiz/trunk/framework/images/webapp/images/flotr/flotr.debug-0.2.0-alpha.js (added) +++ ofbiz/trunk/framework/images/webapp/images/flotr/flotr.debug-0.2.0-alpha.js Mon May 17 09:33:34 2010 @@ -0,0 +1,3105 @@ +//Flotr 0.2.0-alpha Copyright (c) 2009 Bas Wenneker, <http://solutoire.com>, MIT License. +/* $Id: flotr.js 82 2009-01-12 19:19:31Z fabien.menager $ */ + +var Flotr = { + version: '0.2.0-alpha', + author: 'Bas Wenneker', + website: 'http://www.solutoire.com', + /** + * An object of the default registered graph types. Use Flotr.register(type, functionName) + * to add your own type. + */ + _registeredTypes:{ + 'lines': 'drawSeriesLines', + 'points': 'drawSeriesPoints', + 'bars': 'drawSeriesBars', + 'candles': 'drawSeriesCandles', + 'pie': 'drawSeriesPie' + }, + /** + * Can be used to register your own chart type. Default types are 'lines', 'points' and 'bars'. + * This is still experimental. + * @todo Test and confirm. + * @param {String} type - type of chart, like 'pies', 'bars' etc. + * @param {String} functionName - Name of the draw function, like 'drawSeriesPies', 'drawSeriesBars' etc. + */ + register: function(type, functionName){ + Flotr._registeredTypes[type] = functionName+''; + }, + /** + * Draws the graph. This function is here for backwards compatibility with Flotr version 0.1.0alpha. + * You could also draw graphs by directly calling Flotr.Graph(element, data, options). + * @param {Element} el - element to insert the graph into + * @param {Object} data - an array or object of dataseries + * @param {Object} options - an object containing options + * @param {Class} _GraphKlass_ - (optional) Class to pass the arguments to, defaults to Flotr.Graph + * @return {Class} returns a new graph object and of course draws the graph. + */ + draw: function(el, data, options, _GraphKlass_){ + _GraphKlass_ = _GraphKlass_ || Flotr.Graph; + return new _GraphKlass_(el, data, options); + }, + /** + * Collects dataseries from input and parses the series into the right format. It returns an Array + * of Objects each having at least the 'data' key set. + * @param {Array/Object} data - Object or array of dataseries + * @return {Array} Array of Objects parsed into the right format ({(...,) data: [[x1,y1], [x2,y2], ...] (, ...)}) + */ + getSeries: function(data){ + return data.collect(function(serie){ + var i, serie = (serie.data) ? Object.clone(serie) : {'data': serie}; + for (i = serie.data.length-1; i > -1; --i) { + serie.data[i][1] = (serie.data[i][1] === null ? null : parseFloat(serie.data[i][1])); + } + return serie; + }); + }, + /** + * Recursively merges two objects. + * @param {Object} src - source object (likely the object with the least properties) + * @param {Object} dest - destination object (optional, object with the most properties) + * @return {Object} recursively merged Object + */ + merge: function(src, dest){ + var result = dest || {}; + for(var i in src){ + result[i] = (src[i] != null && typeof(src[i]) == 'object' && !(src[i].constructor == Array || src[i].constructor == RegExp) && !Object.isElement(src[i])) ? Flotr.merge(src[i], dest[i]) : result[i] = src[i]; + } + return result; + }, + /** + * Function calculates the ticksize and returns it. + * @param {Integer} noTicks - number of ticks + * @param {Integer} min - lower bound integer value for the current axis + * @param {Integer} max - upper bound integer value for the current axis + * @param {Integer} decimals - number of decimals for the ticks + * @return {Integer} returns the ticksize in pixels + */ + getTickSize: function(noTicks, min, max, decimals){ + var delta = (max - min) / noTicks; + var magn = Flotr.getMagnitude(delta); + + // Norm is between 1.0 and 10.0. + var norm = delta / magn; + + var tickSize = 10; + if(norm < 1.5) tickSize = 1; + else if(norm < 2.25) tickSize = 2; + else if(norm < 3) tickSize = ((decimals == 0) ? 2 : 2.5); + else if(norm < 7.5) tickSize = 5; + + return tickSize * magn; + }, + /** + * Default tick formatter. + * @param {String/Integer} val - tick value integer + * @return {String} formatted tick string + */ + defaultTickFormatter: function(val){ + return val+''; + }, + /** + * Formats the mouse tracker values. + * @param {Object} obj - Track value Object {x:..,y:..} + * @return {String} Formatted track string + */ + defaultTrackFormatter: function(obj){ + return '('+obj.x+', '+obj.y+')'; + }, + defaultPieLabelFormatter: function(slice) { + return (slice.fraction*100).toFixed(2)+'%'; + }, + /** + * Returns the magnitude of the input value. + * @param {Integer/Float} x - integer or float value + * @return {Integer/Float} returns the magnitude of the input value + */ + getMagnitude: function(x){ + return Math.pow(10, Math.floor(Math.log(x) / Math.LN10)); + }, + toPixel: function(val){ + return Math.floor(val)+0.5;//((val-Math.round(val) < 0.4) ? (Math.floor(val)-0.5) : val); + }, + toRad: function(angle){ + return -angle * (Math.PI/180); + }, + /** + * Parses a color string and returns a corresponding Color. + * @param {String} str - string thats representing a color + * @return {Color} returns a Color object or false + */ + parseColor: function(str){ + if (str instanceof Flotr.Color) return str; + + var result, Color = Flotr.Color; + + // 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]), parseInt(result[2]), parseInt(result[3])); + + // rgba(num,num,num,num) + if((result = /rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str))) + return new Color(parseInt(result[1]), parseInt(result[2]), parseInt(result[3]), parseFloat(result[4])); + + // rgb(num%,num%,num%) + if((result = /rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(str))) + return new Color(parseFloat(result[1])*2.55, parseFloat(result[2])*2.55, parseFloat(result[3])*2.55); + + // rgba(num%,num%,num%,num) + if((result = /rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str))) + return new Color(parseFloat(result[1])*2.55, parseFloat(result[2])*2.55, parseFloat(result[3])*2.55, parseFloat(result[4])); + + // #a0b1c2 + if((result = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(str))) + return new Color(parseInt(result[1],16), parseInt(result[2],16), parseInt(result[3],16)); + + // #fff + if((result = /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(str))) + return new Color(parseInt(result[1]+result[1],16), parseInt(result[2]+result[2],16), parseInt(result[3]+result[3],16)); + + // Otherwise, we're most likely dealing with a named color. + var name = str.strip().toLowerCase(); + if(name == 'transparent'){ + return new Color(255, 255, 255, 0); + } + return ((result = Color.lookupColors[name])) ? new Color(result[0], result[1], result[2]) : false; + }, + /** + * Extracts the background-color of the passed element. + * @param {Element} element + * @return {String} color string + */ + extractColor: function(element){ + var color; + // Loop until we find an element with a background color and stop when we hit the body element. + do { + color = element.getStyle('background-color').toLowerCase(); + if(!(color == '' || color == 'transparent')) break; + element = element.up(0); + } while(!element.nodeName.match(/^body$/i)); + + // Catch Safari's way of signaling transparent. + return (color == 'rgba(0, 0, 0, 0)') ? 'transparent' : color; + } +}; +/** + * Flotr Graph class that plots a graph on creation. + + */ +Flotr.Graph = Class.create({ + /** + * Flotr Graph constructor. + * @param {Element} el - element to insert the graph into + * @param {Object} data - an array or object of dataseries + * @param {Object} options - an object containing options + */ + initialize: function(el, data, options){ + this.el = $(el); + + if (!this.el) throw 'The target container doesn\'t exist'; + + this.data = data; + this.series = Flotr.getSeries(data); + this.setOptions(options); + + // Initialize some variables + this.lastMousePos = { pageX: null, pageY: null }; + this.selection = { first: { x: -1, y: -1}, second: { x: -1, y: -1} }; + this.prevSelection = null; + this.selectionInterval = null; + this.ignoreClick = false; + this.prevHit = null; + + // Create and prepare canvas. + this.constructCanvas(); + + // Add event handlers for mouse tracking, clicking and selection + this.initEvents(); + + this.findDataRanges(); + this.calculateTicks(this.axes.x); + this.calculateTicks(this.axes.x2); + this.calculateTicks(this.axes.y); + this.calculateTicks(this.axes.y2); + + this.calculateSpacing(); + this.draw(); + this.insertLegend(); + + // Graph and Data tabs + if (this.options.spreadsheet.show) + this.constructTabs(); + }, + /** + * Sets options and initializes some variables and color specific values, used by the constructor. + * @param {Object} opts - options object + */ + setOptions: function(opts){ + var options = { + colors: ['#00A8F0', '#C0D800', '#CB4B4B', '#4DA74D', '#9440ED'], //=> The default colorscheme. When there are > 5 series, additional colors are generated. + title: null, + subtitle: null, + legend: { + show: true, // => setting to true will show the legend, hide otherwise + noColumns: 1, // => number of colums in legend table // @todo: doesn't work for HtmlText = false + labelFormatter: Prototype.K, // => fn: string -> string + labelBoxBorderColor: '#CCCCCC', // => border color for the little label boxes + labelBoxWidth: 14, + labelBoxHeight: 10, + labelBoxMargin: 5, + container: null, // => container (as jQuery object) to put legend in, null means default on top of graph + position: 'nw', // => 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, set to 1 for a solid background + }, + xaxis: { + ticks: null, // => format: either [1, 3] or [[1, 'a'], 3] + showLabels: true, // => setting to true will show the axis ticks labels, hide otherwise + labelsAngle: 0, // => Labels' angle, in degrees + title: null, // => axis title + titleAngle: 0, // => axis title's angle, in degrees + noTicks: 5, // => number of ticks for automagically generated ticks + tickFormatter: Flotr.defaultTickFormatter, // => fn: number -> string + tickDecimals: null, // => no. of decimals, null means auto + min: null, // => min. value to show, null means set automatically + max: null, // => max. value to show, null means set automatically + autoscaleMargin: 0, // => margin in % to add if auto-setting min/max + color: null + }, + x2axis: {}, + yaxis: { + ticks: null, // => format: either [1, 3] or [[1, 'a'], 3] + showLabels: true, // => setting to true will show the axis ticks labels, hide otherwise + labelsAngle: 0, // => Labels' angle, in degrees + title: null, // => axis title + titleAngle: 90, // => axis title's angle, in degrees + noTicks: 5, // => number of ticks for automagically generated ticks + tickFormatter: Flotr.defaultTickFormatter, // => fn: number -> string + tickDecimals: null, // => no. of decimals, null means auto + min: null, // => min. value to show, null means set automatically + max: null, // => max. value to show, null means set automatically + autoscaleMargin: 0, // => margin in % to add if auto-setting min/max + color: null + }, + y2axis: { + titleAngle: 270 + }, + points: { + show: false, // => setting to true will show points, false will hide + radius: 3, // => point radius (pixels) + lineWidth: 2, // => line width in pixels + fill: true, // => true to fill the points with a color, false for (transparent) no fill + fillColor: '#FFFFFF', // => fill color + fillOpacity: 0.4 + }, + lines: { + show: false, // => setting to true will show lines, false will hide + lineWidth: 2, // => line width in pixels + fill: false, // => true to fill the area from the line to the x axis, false for (transparent) no fill + fillColor: null, // => fill color + fillOpacity: 0.4 // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill + }, + bars: { + show: false, // => setting to true will show bars, false will hide + lineWidth: 2, // => in pixels + barWidth: 1, // => in units of the x axis + fill: true, // => true to fill the area from the line to the x axis, false for (transparent) no fill + fillColor: null, // => fill color + fillOpacity: 0.4, // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill + horizontal: false, + stacked: false + }, + candles: { + show: false, // => setting to true will show candle sticks, false will hide + lineWidth: 1, // => in pixels + wickLineWidth: 1, // => in pixels + candleWidth: 0.6, // => in units of the x axis + fill: true, // => true to fill the area from the line to the x axis, false for (transparent) no fill + upFillColor: '#00A8F0',// => up sticks fill color + downFillColor: '#CB4B4B',// => down sticks fill color + fillOpacity: 0.5, // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill + barcharts: false // => draw as barcharts (not standard bars but financial barcharts) + }, + pie: { + show: false, // => setting to true will show bars, false will hide + lineWidth: 1, // => in pixels + fill: true, // => true to fill the area from the line to the x axis, false for (transparent) no fill + fillColor: null, // => fill color + fillOpacity: 0.6, // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill + explode: 6, + sizeRatio: 0.6, + startAngle: Math.PI/4, + labelFormatter: Flotr.defaultPieLabelFormatter, + pie3D: false, + pie3DviewAngle: (Math.PI/2 * 0.8), + pie3DspliceThickness: 20 + }, + grid: { + color: '#545454', // => primary color used for outline and labels + backgroundColor: null, // => null for transparent, else color + tickColor: '#DDDDDD', // => color used for the ticks + labelMargin: 3, // => margin in pixels + verticalLines: true, // => whether to show gridlines in vertical direction + horizontalLines: true, // => whether to show gridlines in horizontal direction + outlineWidth: 2 // => width of the grid outline/border in pixels + }, + selection: { + mode: null, // => one of null, 'x', 'y' or 'xy' + color: '#B6D9FF', // => selection box color + fps: 20 // => frames-per-second + }, + mouse: { + track: false, // => true to track the mouse, no tracking otherwise + position: 'se', // => position of the value box (default south-east) + relative: false, // => next to the mouse cursor + trackFormatter: Flotr.defaultTrackFormatter, // => formats the values in the value box + margin: 5, // => margin in pixels of the valuebox + lineColor: '#FF3F19', // => line color of points that are drawn when mouse comes near a value of a series + trackDecimals: 1, // => decimals for the track values + sensibility: 2, // => the lower this number, the more precise you have to aim to show a value + radius: 3 // => radius of the track point + }, + shadowSize: 4, // => size of the 'fake' shadow + defaultType: 'lines', // => default series type + HtmlText: true, // => wether to draw the text using HTML or on the canvas + fontSize: 7.5, // => canvas' text font size + spreadsheet: { + show: false, // => show the data grid using two tabs + tabGraphLabel: 'Graph', + tabDataLabel: 'Data', + toolbarDownload: 'Download CSV', // @todo: add language support + toolbarSelectAll: 'Select all' + } + } + + options.x2axis = Object.extend(Object.clone(options.xaxis), options.x2axis); + options.y2axis = Object.extend(Object.clone(options.yaxis), options.y2axis); + this.options = Flotr.merge((opts || {}), options); + + this.axes = { + x: {options: this.options.xaxis, n: 1}, + x2: {options: this.options.x2axis, n: 2}, + y: {options: this.options.yaxis, n: 1}, + y2: {options: this.options.y2axis, n: 2} + }; + + // Initialize some variables used throughout this function. + var assignedColors = [], + colors = [], + ln = this.series.length, + neededColors = this.series.length, + oc = this.options.colors, + usedColors = [], + variation = 0, + c, i, j, s, tooClose; + + // Collect user-defined colors from series. + for(i = neededColors - 1; i > -1; --i){ + c = this.series[i].color; + if(c != null){ + --neededColors; + if(Object.isNumber(c)) assignedColors.push(c); + else usedColors.push(Flotr.parseColor(c)); + } + } + + // Calculate the number of colors that need to be generated. + for(i = assignedColors.length - 1; i > -1; --i) + neededColors = Math.max(neededColors, assignedColors[i] + 1); + + // Generate needed number of colors. + for(i = 0; colors.length < neededColors;){ + c = (oc.length == i) ? new Flotr.Color(100, 100, 100) : Flotr.parseColor(oc[i]); + + // Make sure each serie gets a different color. + var sign = variation % 2 == 1 ? -1 : 1; + var factor = 1 + sign * Math.ceil(variation / 2) * 0.2; + c.scale(factor, factor, factor); + + /** + * @todo if we're getting too close to something else, we should probably skip this one + */ + colors.push(c); + + if(++i >= oc.length){ + i = 0; + ++variation; + } + } + + // Fill the options with the generated colors. + for(i = 0, j = 0; i < ln; ++i){ + s = this.series[i]; + + // Assign the color. + if(s.color == null){ + s.color = colors[j++].toString(); + }else if(Object.isNumber(s.color)){ + s.color = colors[s.color].toString(); + } + + if (!s.xaxis) s.xaxis = this.axes.x; + if (s.xaxis == 1) s.xaxis = this.axes.x; + else if (s.xaxis == 2) s.xaxis = this.axes.x2; + + if (!s.yaxis) s.yaxis = this.axes.y; + if (s.yaxis == 1) s.yaxis = this.axes.y; + else if (s.yaxis == 2) s.yaxis = this.axes.y2; + + // Apply missing options to the series. + s.lines = Object.extend(Object.clone(this.options.lines), s.lines); + s.points = Object.extend(Object.clone(this.options.points), s.points); + s.bars = Object.extend(Object.clone(this.options.bars), s.bars); + s.candles = Object.extend(Object.clone(this.options.candles), s.candles); + s.pie = Object.extend(Object.clone(this.options.pie), s.pie); + s.mouse = Object.extend(Object.clone(this.options.mouse), s.mouse); + + if(s.shadowSize == null) s.shadowSize = this.options.shadowSize; + } + }, + /** + * Initializes the canvas and it's overlay canvas element. When the browser is IE, this makes use + * of excanvas. The overlay canvas is inserted for displaying interactions. After the canvas elements + * are created, the elements are inserted into the container element. + */ + constructCanvas: function(){ + var el = this.el, + size, c, oc; + + this.canvas = el.select('.flotr-canvas')[0]; + this.overlay = el.select('.flotr-overlay')[0]; + + el.childElements().invoke('remove'); + + // For positioning labels and overlay. + el.setStyle({position:'relative', cursor:'default'}); + + this.canvasWidth = el.getWidth(); + this.canvasHeight = el.getHeight(); + size = {'width': this.canvasWidth, 'height': this.canvasHeight}; + + if(this.canvasWidth <= 0 || this.canvasHeight <= 0){ + throw 'Invalid dimensions for plot, width = ' + this.canvasWidth + ', height = ' + this.canvasHeight; + } + + // Insert main canvas. + if (!this.canvas) { + c = this.canvas = new Element('canvas', size); + c.className = 'flotr-canvas'; + c = c.writeAttribute('style', 'position:absolute;left:0px;top:0px;'); + } else { + c = this.canvas.writeAttribute(size); + } + el.insert(c); + + if(Prototype.Browser.IE){ + c = window.G_vmlCanvasManager.initElement(c); + } + this.ctx = c.getContext('2d'); + + // Insert overlay canvas for interactive features. + if (!this.overlay) { + oc = this.overlay = new Element('canvas', size); + oc.className = 'flotr-overlay'; + oc = oc.writeAttribute('style', 'position:absolute;left:0px;top:0px;'); + } else { + oc = this.overlay.writeAttribute(size); + } + el.insert(oc); + + if(Prototype.Browser.IE){ + oc = window.G_vmlCanvasManager.initElement(oc); + } + this.octx = oc.getContext('2d'); + + // Enable text functions + if (window.CanvasText) { + CanvasText.enable(this.ctx); + CanvasText.enable(this.octx); + this.textEnabled = true; + } + }, + getTextDimensions: function(text, canvasStyle, HtmlStyle, className) { + if (!text) return {width:0, height:0}; + + if (!this.options.HtmlText && this.textEnabled) { + var bounds = this.ctx.getTextBounds(text, canvasStyle); + return { + width: bounds.width+2, + height: bounds.height+6 + }; + } + else { + var dummyDiv = this.el.insert('<div style="position:absolute;top:-10000px;'+HtmlStyle+'" class="'+className+' flotr-dummy-div">' + text + '</div>').select(".flotr-dummy-div")[0]; + dim = dummyDiv.getDimensions(); + dummyDiv.remove(); + return dim; + } + }, + loadDataGrid: function(){ + if (this.seriesData) return this.seriesData; + + var s = this.series; + var dg = []; + + /* The data grid is a 2 dimensions array. There is a row for each X value. + * Each row contains the x value and the corresponding y value for each serie ('undefined' if there isn't one) + **/ + for(i = 0; i < s.length; ++i){ + s[i].data.each(function(v) { + var x = v[0], + y = v[1]; + if (r = dg.find(function(row) {return row[0] == x})) { + r[i+1] = y; + } + else { + var newRow = []; + newRow[0] = x; + newRow[i+1] = y + dg.push(newRow); + } + }); + } + + // The data grid is sorted by x value + dg = dg.sortBy(function(v) { + return v[0]; + }); + return this.seriesData = dg; + }, + + // @todo: make a tab manager (Flotr.Tabs) + showTab: function(tabName, onComplete){ + var elementsClassNames = 'canvas, .flotr-labels, .flotr-legend, .flotr-legend-bg, .flotr-title, .flotr-subtitle'; + switch(tabName) { + case 'graph': + this.datagrid.up().hide(); + this.el.select(elementsClassNames).invoke('show'); + this.tabs.data.removeClassName('selected'); + this.tabs.graph.addClassName('selected'); + break; + case 'data': + this.constructDataGrid(); + this.datagrid.up().show(); + this.el.select(elementsClassNames).invoke('hide'); + this.tabs.data.addClassName('selected'); + this.tabs.graph.removeClassName('selected'); + break; + } + }, + constructTabs: function(){ + var tabsContainer = new Element('div', {className:'flotr-tabs-group', style:'position:absolute;left:0px;top:'+this.canvasHeight+'px;width:'+this.canvasWidth+'px;'}); + this.el.insert({bottom: tabsContainer}); + this.tabs = { + graph: new Element('div', {className:'flotr-tab selected', style:'float:left;'}).update(this.options.spreadsheet.tabGraphLabel), + data: new Element('div', {className:'flotr-tab', style:'float:left;'}).update(this.options.spreadsheet.tabDataLabel) + } + + tabsContainer.insert(this.tabs.graph).insert(this.tabs.data); + + this.el.setStyle({height: this.canvasHeight+this.tabs.data.getHeight()+2+'px'}); + + this.tabs.graph.observe('click', (function() {this.showTab('graph')}).bind(this)); + this.tabs.data.observe('click', (function() {this.showTab('data')}).bind(this)); + }, + + // @todo: make a spreadsheet manager (Flotr.Spreadsheet) + constructDataGrid: function(){ + // If the data grid has already been built, nothing to do here + if (this.datagrid) return this.datagrid; + + var i, j, + s = this.series, + datagrid = this.loadDataGrid(); + + var t = this.datagrid = new Element('table', {className:'flotr-datagrid', style:'height:100px;'}); + var colgroup = ['<colgroup><col />']; + + // First row : series' labels + var html = ['<tr class="first-row">']; + html.push('<th> </th>'); + for (i = 0; i < s.length; ++i) { + html.push('<th scope="col">'+(s[i].label || String.fromCharCode(65+i))+'</th>'); + colgroup.push('<col />'); + } + html.push('</tr>'); + + // Data rows + for (j = 0; j < datagrid.length; ++j) { + html.push('<tr>'); + for (i = 0; i < s.length+1; ++i) { + var tag = 'td'; + var content = (datagrid[j][i] != null ? Math.round(datagrid[j][i]*100000)/100000 : ''); + + if (i == 0) { + tag = 'th'; + var label; + if(this.options.xaxis.ticks) { + var tick = this.options.xaxis.ticks.find(function (x) { return x[0] == datagrid[j][i] }); + if (tick) label = tick[1]; + } + else { + label = this.options.xaxis.tickFormatter(content); + } + + if (label) content = label; + } + + html.push('<'+tag+(tag=='th'?' scope="row"':'')+'>'+content+'</'+tag+'>'); + } + html.push('</tr>'); + } + colgroup.push('</colgroup>'); + t.update(colgroup.join('')+html.join('')); + + if (!Prototype.Browser.IE) { + t.select('td').each(function(td) { + td.observe('mouseover', function(e){ + td = e.element(); + var siblings = td.previousSiblings(); + + t.select('th[scope=col]')[siblings.length-1].addClassName('hover'); + t.select('colgroup col')[siblings.length].addClassName('hover'); + }); + + td.observe('mouseout', function(){ + t.select('colgroup col.hover, th.hover').each(function(e){e.removeClassName('hover')}); + }); + }); + } + + var toolbar = new Element('div', {className: 'flotr-datagrid-toolbar'}). + insert(new Element('button', {type:'button', className:'flotr-datagrid-toolbar-button'}).update(this.options.spreadsheet.toolbarDownload).observe('click', this.downloadCSV.bind(this))). + insert(new Element('button', {type:'button', className:'flotr-datagrid-toolbar-button'}).update(this.options.spreadsheet.toolbarSelectAll).observe('click', this.selectAllData.bind(this))); + + var container = new Element('div', {className:'flotr-datagrid-container', style:'left:0px;top:0px;width:'+this.canvasWidth+'px;height:'+this.canvasHeight+'px;overflow:auto;'}); + container.insert(toolbar); + t.wrap(container.hide()); + + this.el.insert(container); + return t; + }, + selectAllData: function(){ + if (this.tabs) { + var selection, range, doc, win, node = this.constructDataGrid(); + + this.showTab('data'); + + // deferred to be able to select the table + (function () { + if ((doc = node.ownerDocument) && (win = doc.defaultView) && + win.getSelection && doc.createRange && + (selection = window.getSelection()) && + selection.removeAllRanges) { + range = doc.createRange(); + range.selectNode(node); + selection.removeAllRanges(); + selection.addRange(range); + } + else if (document.body && document.body.createTextRange && + (range = document.body.createTextRange())) { + range.moveToElementText(node); + range.select(); + } + }).defer(); + return true; + } + else return false; + }, + downloadCSV: function(){ + var i, csv = '"x"', + series = this.series, + dg = this.loadDataGrid(); + + for (i = 0; i < series.length; ++i) { + csv += '%09"'+(series[i].label || String.fromCharCode(65+i))+'"'; // \t + } + csv += "%0D%0A"; // \r\n + + for (i = 0; i < dg.length; ++i) { + if (this.options.xaxis.ticks) { + var tick = this.options.xaxis.ticks.find(function (x) { return x[0] == dg[i][0] }); + if (tick) dg[i][0] = tick[1]; + } else { + dg[i][0] = this.options.xaxis.tickFormatter(dg[i][0]); + } + csv += dg[i].join('%09')+"%0D%0A"; // \t and \r\n + } + if (Prototype.Browser.IE) { + csv = csv.gsub('%09', '\t').gsub('%0A', '\n').gsub('%0D', '\r'); + window.open().document.write(csv); + } + else { + window.open('data:text/csv,'+csv); + } + }, + /** + * Initializes event some handlers. + */ + initEvents: function () { + //@todo: maybe stopObserving with only flotr functions + this.overlay.stopObserving(); + this.overlay.observe('mousedown', this.mouseDownHandler.bind(this)); + this.overlay.observe('mousemove', this.mouseMoveHandler.bind(this)); + this.overlay.observe('click', this.clickHandler.bind(this)); + }, + /** + * Function determines the min and max values for the xaxis and yaxis. + */ + findDataRanges: function(){ + var s = this.series, + a = this.axes; + + a.x.datamin = 0; a.x.datamax = 0; + a.x2.datamin = 0; a.x2.datamax = 0; + a.y.datamin = 0; a.y.datamax = 0; + a.y2.datamin = 0; a.y2.datamax = 0; + + if(s.length > 0){ + var i, j, h, x, y, data, xaxis, yaxis; + + // Get datamin, datamax start values + for(i = 0; i < s.length; ++i) { + data = s[i].data, + xaxis = s[i].xaxis, + yaxis = s[i].yaxis; + + if (data.length > 0 && !s[i].hide) { + if (!xaxis.used) xaxis.datamin = xaxis.datamax = data[0][0]; + if (!yaxis.used) yaxis.datamin = yaxis.datamax = data[0][1]; + xaxis.used = true; + yaxis.used = true; + + for(h = data.length - 1; h > -1; --h){ + x = data[h][0]; + if(x < xaxis.datamin) xaxis.datamin = x; + else if(x > xaxis.datamax) xaxis.datamax = x; + + for(j = 1; j < data[h].length; j++){ + y = data[h][j]; + if(y < yaxis.datamin) yaxis.datamin = y; + else if(y > yaxis.datamax) yaxis.datamax = y; + } + } + } + } + } + + this.findXAxesValues(); + + this.calculateRange(a.x); + this.extendXRangeIfNeededByBar(a.x); + + if (a.x2.used) { + this.calculateRange(a.x2); + this.extendXRangeIfNeededByBar(a.x2); + } + + this.calculateRange(a.y); + this.extendYRangeIfNeededByBar(a.y); + + if (a.y2.used) { + this.calculateRange(a.y2); + this.extendYRangeIfNeededByBar(a.y2); + } + }, + /** + * Calculates the range of an axis to apply autoscaling. + */ + calculateRange: function(axis){ + var o = axis.options, + min = o.min != null ? o.min : axis.datamin, + max = o.max != null ? o.max : axis.datamax, + margin; + + if(max - min == 0.0){ + var widen = (max == 0.0) ? 1.0 : 0.01; + min -= widen; + max += widen; + } + axis.tickSize = Flotr.getTickSize(o.noTicks, min, max, o.tickDecimals); + + // Autoscaling. + if(o.min == null){ + // Add a margin. + margin = o.autoscaleMargin; + if(margin != 0){ + min -= axis.tickSize * margin; + + // Make sure we don't go below zero if all values are positive. + if(min < 0 && axis.datamin >= 0) min = 0; + min = axis.tickSize * Math.floor(min / axis.tickSize); + } + } + if(o.max == null){ + margin = o.autoscaleMargin; + if(margin != 0){ + max += axis.tickSize * margin; + if(max > 0 && axis.datamax <= 0) max = 0; + max = axis.tickSize * Math.ceil(max / axis.tickSize); + } + } + axis.min = min; + axis.max = max; + }, + /** + * Bar series autoscaling in x direction. + */ + extendXRangeIfNeededByBar: function(axis){ + if(axis.options.max == null){ + var newmax = axis.max, + i, s, b, c, + stackedSums = [], + lastSerie = null; + + for(i = 0; i < this.series.length; ++i){ + s = this.series[i]; + b = s.bars; + c = s.candles; + if(s.axis == axis && (b.show || c.show)) { + if (!b.horizontal && (b.barWidth + axis.datamax > newmax) || (c.candleWidth + axis.datamax > newmax)){ + newmax = axis.max + s.bars.barWidth; + } + if(b.stacked && b.horizontal){ + for (j = 0; j < s.data.length; j++) { + if (s.bars.show && s.bars.stacked) { + var x = s.data[j][0]; + stackedSums[x] = (stackedSums[x] || 0) + s.data[j][1]; + lastSerie = s; + } + } + + for (j = 0; j < stackedSums.length; j++) { + newmax = Math.max(stackedSums[j], newmax); + } + } + } + } + axis.lastSerie = lastSerie; + axis.max = newmax; + } + }, + /** + * Bar series autoscaling in y direction. + */ + extendYRangeIfNeededByBar: function(axis){ + if(axis.options.max == null){ + var newmax = axis.max, + i, s, b, c, + stackedSums = [], + lastSerie = null; + + for(i = 0; i < this.series.length; ++i){ + s = this.series[i]; + b = s.bars; + c = s.candles; + if (s.yaxis == axis && b.show && !s.hide) { + if (b.horizontal && (b.barWidth + axis.datamax > newmax) || (c.candleWidth + axis.datamax > newmax)){ + newmax = axis.max + b.barWidth; + } + if(b.stacked && !b.horizontal){ + for (j = 0; j < s.data.length; j++) { + if (s.bars.show && s.bars.stacked) { + var x = s.data[j][0]; + stackedSums[x] = (stackedSums[x] || 0) + s.data[j][1]; + lastSerie = s; + } + } + + for (j = 0; j < stackedSums.length; j++) { + newmax = Math.max(stackedSums[j], newmax); + } + } + } + } + axis.lastSerie = lastSerie; + axis.max = newmax; + } + }, + /** + * Find every values of the x axes + */ + findXAxesValues: function(){ + for(i = this.series.length-1; i > -1 ; --i){ + s = this.series[i]; + s.xaxis.values = s.xaxis.values || []; + for (j = s.data.length-1; j > -1 ; --j){ + s.xaxis.values[s.data[j][0]] = {}; + } + } + }, + /** + * Calculate axis ticks. + * @param {Object} axis - axis object + * @param {Object} o - axis options + */ + calculateTicks: function(axis){ + var o = axis.options, i, v; + + axis.ticks = []; + if(o.ticks){ + var ticks = o.ticks, t, label; + + if(Object.isFunction(ticks)){ + ticks = ticks({min: axis.min, max: axis.max}); + } + + // Clean up the user-supplied ticks, copy them over. + for(i = 0; i < ticks.length; ++i){ + t = ticks[i]; + if(typeof(t) == 'object'){ + v = t[0]; + label = (t.length > 1) ? t[1] : o.tickFormatter(v); + }else{ + v = t; + label = o.tickFormatter(v); + } + axis.ticks[i] = { v: v, label: label }; + } + } + else { + // Round to nearest multiple of tick size. + var start = axis.tickSize * Math.ceil(axis.min / axis.tickSize), + decimals; + + // Then store all possible ticks. + for(i = 0; start + i * axis.tickSize <= axis.max; ++i){ + v = start + i * axis.tickSize; + + // Round (this is always needed to fix numerical instability). + decimals = o.tickDecimals; + if(decimals == null) decimals = 1 - Math.floor(Math.log(axis.tickSize) / Math.LN10); + if(decimals < 0) decimals = 0; + + v = v.toFixed(decimals); + axis.ticks.push({ v: v, label: o.tickFormatter(v) }); + } + } + }, + /** + * Calculates axis label sizes. + */ + calculateSpacing: function(){ + var a = this.axes, + options = this.options, + series = this.series, + margin = options.grid.labelMargin, + x = a.x, + x2 = a.x2, + y = a.y, + y2 = a.y2, + maxOutset = 2, + i, j, l, dim; + + // Labels width and height + [x, x2, y, y2].each(function(axis) { + var maxLabel = ''; + + if (axis.options.showLabels) { + for(i = 0; i < axis.ticks.length; ++i){ + l = axis.ticks[i].label.length; + if(l > maxLabel.length){ + maxLabel = axis.ticks[i].label; + } + } + } + axis.maxLabel = this.getTextDimensions(maxLabel, {size:options.fontSize, angle: Flotr.toRad(axis.options.labelsAngle)}, 'font-size:smaller;', 'flotr-grid-label'); + axis.titleSize = this.getTextDimensions(axis.options.title, {size: options.fontSize*1.2, angle: Flotr.toRad(axis.options.titleAngle)}, 'font-weight:bold;', 'flotr-axis-title'); + }, this); + + // Title height + dim = this.getTextDimensions(options.title, {size: options.fontSize*1.5}, 'font-size:1em;font-weight:bold;', 'flotr-title'); + this.titleHeight = dim.height; + + // Subtitle height + dim = this.getTextDimensions(options.subtitle, {size: options.fontSize}, 'font-size:smaller;', 'flotr-subtitle'); + this.subtitleHeight = dim.height; + + // Grid outline line width. + if(options.show){ + maxOutset = Math.max(maxOutset, options.points.radius + options.points.lineWidth/2); + } + for(j = 0; j < options.length; ++j){ + if (series[j].points.show){ + maxOutset = Math.max(maxOutset, series[j].points.radius + series[j].points.lineWidth/2); + } + } + + var p = this.plotOffset = {left: 0, right: 0, top: 0, bottom: 0}; + p.left = p.right = p.top = p.bottom = maxOutset; + + p.bottom += (x.options.showLabels ? (x.maxLabel.height + margin) : 0) + + (x.options.title ? (x.titleSize.height + margin) : 0); + + p.top += (x2.options.showLabels ? (x2.maxLabel.height + margin) : 0) + + (x2.options.title ? (x2.titleSize.height + margin) : 0) + this.subtitleHeight + this.titleHeight; + + p.left += (y.options.showLabels ? (y.maxLabel.width + margin) : 0) + + (y.options.title ? (y.titleSize.width + margin) : 0); + + p.right += (y2.options.showLabels ? (y2.maxLabel.width + margin) : 0) + + (y2.options.title ? (y2.titleSize.width + margin) : 0); + + p.top = Math.floor(p.top); // In order the outline not to be blured + + this.plotWidth = this.canvasWidth - p.left - p.right; + this.plotHeight = this.canvasHeight - p.bottom - p.top; + + x.scale = this.plotWidth / (x.max - x.min); + x2.scale = this.plotWidth / (x2.max - x2.min); + y.scale = this.plotHeight / (y.max - y.min); + y2.scale = this.plotHeight / (y2.max - y2.min); + }, + /** + * Draws grid, labels and series. + */ + draw: function() { + this.drawGrid(); + this.drawLabels(); + this.drawTitles(); + + if(this.series.length){ + this.el.fire('flotr:beforedraw', [this.series, this]); + for(var i = 0; i < this.series.length; i++){ + if (!this.series[i].hide) + this.drawSeries(this.series[i]); + } + } + this.el.fire('flotr:afterdraw', [this.series, this]); + }, + /** + * Translates absolute horizontal x coordinates to relative coordinates. + * @param {Integer} x - absolute integer x coordinate + * @return {Integer} translated relative x coordinate + */ + tHoz: function(x, axis){ + axis = axis || this.axes.x; + return (x - axis.min) * axis.scale; + }, + /** + * Translates absolute vertical x coordinates to relative coordinates. + * @param {Integer} y - absolute integer y coordinate + * @return {Integer} translated relative y coordinate + */ + tVert: function(y, axis){ + axis = axis || this.axes.y; + return this.plotHeight - (y - axis.min) * axis.scale; + }, + /** + * Draws a grid for the graph. + */ + drawGrid: function(){ + var v, o = this.options, + ctx = this.ctx; + if(o.grid.verticalLines || o.grid.horizontalLines){ + this.el.fire('flotr:beforegrid', [this.axes.x, this.axes.y, o, this]); + } + ctx.save(); + ctx.translate(this.plotOffset.left, this.plotOffset.top); + + // Draw grid background, if present in options. + if(o.grid.backgroundColor != null){ + ctx.fillStyle = o.grid.backgroundColor; + ctx.fillRect(0, 0, this.plotWidth, this.plotHeight); + } + + // Draw grid lines in vertical direction. + ctx.lineWidth = 1; + ctx.strokeStyle = o.grid.tickColor; + ctx.beginPath(); + if(o.grid.verticalLines){ + for(var i = 0; i < this.axes.x.ticks.length; ++i){ + v = this.axes.x.ticks[i].v; + // Don't show lines on upper and lower bounds. + if ((v == this.axes.x.min || v == this.axes.x.max) && o.grid.outlineWidth != 0) + continue; + + ctx.moveTo(Math.floor(this.tHoz(v)) + ctx.lineWidth/2, 0); + ctx.lineTo(Math.floor(this.tHoz(v)) + ctx.lineWidth/2, this.plotHeight); + } + } + + // Draw grid lines in horizontal direction. + if(o.grid.horizontalLines){ + for(var j = 0; j < this.axes.y.ticks.length; ++j){ + v = this.axes.y.ticks[j].v; + // Don't show lines on upper and lower bounds. + if ((v == this.axes.y.min || v == this.axes.y.max) && o.grid.outlineWidth != 0) + continue; + + ctx.moveTo(0, Math.floor(this.tVert(v)) + ctx.lineWidth/2); + ctx.lineTo(this.plotWidth, Math.floor(this.tVert(v)) + ctx.lineWidth/2); + } + } + ctx.stroke(); + + // Draw axis/grid border. + if(o.grid.outlineWidth != 0) { + ctx.lineWidth = o.grid.outlineWidth; + ctx.strokeStyle = o.grid.color; + ctx.lineJoin = 'round'; + ctx.strokeRect(0, 0, this.plotWidth, this.plotHeight); + } + ctx.restore(); + if(o.grid.verticalLines || o.grid.horizontalLines){ + this.el.fire('flotr:aftergrid', [this.axes.x, this.axes.y, o, this]); + } + }, + /** + * Draws labels for x and y axis. + */ + drawLabels: function(){ + // Construct fixed width label boxes, which can be styled easily. + var noLabels = 0, axis, + xBoxWidth, i, html, tick, + options = this.options, + ctx = this.ctx, + a = this.axes; + + for(i = 0; i < a.x.ticks.length; ++i){ + if (a.x.ticks[i].label) { + ++noLabels; + } + } + xBoxWidth = this.plotWidth / noLabels; + + if (!options.HtmlText && this.textEnabled) { + var style = { + size: options.fontSize, + adjustAlign: true + }; + + // Add x labels. + axis = a.x; + style.color = axis.options.color || options.grid.color; + for(i = 0; i < axis.ticks.length && axis.options.showLabels && axis.used; ++i){ + tick = axis.ticks[i]; + if(!tick.label || tick.label.length == 0) continue; + + style.angle = Flotr.toRad(axis.options.labelsAngle); + style.halign = 'c'; + style.valign = 't'; + + ctx.drawText( + tick.label, + this.plotOffset.left + this.tHoz(tick.v, axis), + this.plotOffset.top + this.plotHeight + options.grid.labelMargin, + style + ); + } + + // Add x2 labels. + axis = a.x2; + style.color = axis.options.color || options.grid.color; + for(i = 0; i < axis.ticks.length && axis.options.showLabels && axis.used; ++i){ + tick = axis.ticks[i]; + if(!tick.label || tick.label.length == 0) continue; + + style.angle = Flotr.toRad(axis.options.labelsAngle); + style.halign = 'c'; + style.valign = 'b'; + + ctx.drawText( + tick.label, + this.plotOffset.left + this.tHoz(tick.v, axis), + this.plotOffset.top + options.grid.labelMargin, + style + ); + } + + // Add y labels. + axis = a.y; + style.color = axis.options.color || options.grid.color; + for(i = 0; i < axis.ticks.length && axis.options.showLabels && axis.used; ++i){ + tick = axis.ticks[i]; + if (!tick.label || tick.label.length == 0) continue; + + style.angle = Flotr.toRad(axis.options.labelsAngle); + style.halign = 'r'; + style.valign = 'm'; + + ctx.drawText( + tick.label, + this.plotOffset.left - options.grid.labelMargin, + this.plotOffset.top + this.tVert(tick.v, axis), + style + ); + } + + // Add y2 labels. + axis = a.y2; + style.color = axis.options.color || options.grid.color; + for(i = 0; i < axis.ticks.length && axis.options.showLabels && axis.used; ++i){ + tick = axis.ticks[i]; + if (!tick.label || tick.label.length == 0) continue; + + style.angle = Flotr.toRad(axis.options.labelsAngle); + style.halign = 'l'; + style.valign = 'm'; + + ctx.drawText( + tick.label, + this.plotOffset.left + this.plotWidth + options.grid.labelMargin, + this.plotOffset.top + this.tVert(tick.v, axis), + style + ); + + ctx.save(); + ctx.strokeStyle = style.color; + ctx.beginPath(); + ctx.moveTo(this.plotOffset.left + this.plotWidth - 8, this.plotOffset.top + this.tVert(tick.v, axis)); + ctx.lineTo(this.plotOffset.left + this.plotWidth, this.plotOffset.top + this.tVert(tick.v, axis)); + ctx.stroke(); + ctx.restore(); + } + } + else if (a.x.options.showLabels || + a.x2.options.showLabels || + a.y.options.showLabels || + a.y2.options.showLabels) { + html = ['<div style="font-size:smaller;color:' + options.grid.color + ';" class="flotr-labels">']; + + // Add x labels. + axis = a.x; + if (axis.options.showLabels){ + for(i = 0; i < axis.ticks.length; ++i){ + tick = axis.ticks[i]; + if(!tick.label || tick.label.length == 0) continue; + html.push('<div style="position:absolute;top:' + (this.plotOffset.top + this.plotHeight + options.grid.labelMargin) + 'px;left:' + (this.plotOffset.left + this.tHoz(tick.v, axis) - xBoxWidth/2) + 'px;width:' + xBoxWidth + 'px;text-align:center;'+(axis.options.color?('color:'+axis.options.color+';'):'')+'" class="flotr-grid-label">' + tick.label + '</div>'); + } + } + + // Add x2 labels. + axis = a.x2; + if (axis.options.showLabels && axis.used){ + for(i = 0; i < axis.ticks.length; ++i){ + tick = axis.ticks[i]; + if(!tick.label || tick.label.length == 0) continue; + html.push('<div style="position:absolute;top:' + (this.plotOffset.top - options.grid.labelMargin - axis.maxLabel.height) + 'px;left:' + (this.plotOffset.left + this.tHoz(tick.v, axis) - xBoxWidth/2) + 'px;width:' + xBoxWidth + 'px;text-align:center;'+(axis.options.color?('color:'+axis.options.color+';'):'')+'" class="flotr-grid-label">' + tick.label + '</div>'); + } + } + + // Add y labels. + axis = a.y; + if (axis.options.showLabels){ + for(i = 0; i < axis.ticks.length; ++i){ + tick = axis.ticks[i]; + if (!tick.label || tick.label.length == 0) continue; + html.push('<div style="position:absolute;top:' + (this.plotOffset.top + this.tVert(tick.v, axis) - axis.maxLabel.height/2) + 'px;left:0;width:' + (this.plotOffset.left - options.grid.labelMargin) + 'px;text-align:right;'+(axis.options.color?('color:'+axis.options.color+';'):'')+'" class="flotr-grid-label">' + tick.label + '</div>'); + } + } + + // Add y2 labels. + axis = a.y2; + if (axis.options.showLabels && axis.used){ + ctx.save(); + ctx.strokeStyle = axis.options.color || options.grid.color; + ctx.beginPath(); + + for(i = 0; i < axis.ticks.length; ++i){ + tick = axis.ticks[i]; + if (!tick.label || tick.label.length == 0) continue; + html.push('<div style="position:absolute;top:' + (this.plotOffset.top + this.tVert(tick.v, axis) - axis.maxLabel.height/2) + 'px;right:0;width:' + (this.plotOffset.right - options.grid.labelMargin) + 'px;text-align:left;'+(axis.options.color?('color:'+axis.options.color+';'):'')+'" class="flotr-grid-label">' + tick.label + '</div>'); + + ctx.moveTo(this.plotOffset.left + this.plotWidth - 8, this.plotOffset.top + this.tVert(tick.v, axis)); + ctx.lineTo(this.plotOffset.left + this.plotWidth, this.plotOffset.top + this.tVert(tick.v, axis)); + } + ctx.stroke(); + ctx.restore(); + } + + html.push('</div>'); + this.el.insert(html.join('')); + } + }, + /** + * Draws the title and the subtitle + */ + drawTitles: function(){ + var html, + options = this.options, + margin = options.grid.labelMargin, + ctx = this.ctx, + a = this.axes; + + if (!options.HtmlText && this.textEnabled) { + var style = { + size: options.fontSize, + color: options.grid.color, + halign: 'c' + }; + + // Add subtitle + if (options.subtitle){ + ctx.drawText( + options.subtitle, + this.plotOffset.left + this.plotWidth/2, + this.titleHeight + this.subtitleHeight - 2, + style + ); + } + + style.weight = 1.5; + style.size *= 1.5; + + // Add title + if (options.title){ + ctx.drawText( + options.title, + this.plotOffset.left + this.plotWidth/2, + this.titleHeight - 2, + style + ); + } + + style.weight = 1.8; + style.size *= 0.8; + style.adjustAlign = true; + + // Add x axis title + if (a.x.options.title && a.x.used){ + style.halign = 'c'; + style.valign = 't'; + style.angle = Flotr.toRad(a.x.options.titleAngle); + ctx.drawText( + a.x.options.title, + this.plotOffset.left + this.plotWidth/2, + this.plotOffset.top + a.x.maxLabel.height + this.plotHeight + 2 * margin, + style + ); + } + + // Add x2 axis title + if (a.x2.options.title && a.x2.used){ + style.halign = 'c'; + style.valign = 'b'; + style.angle = Flotr.toRad(a.x2.options.titleAngle); + ctx.drawText( + a.x2.options.title, + this.plotOffset.left + this.plotWidth/2, + this.plotOffset.top - a.x2.maxLabel.height - 2 * margin, + style + ); + } + + // Add y axis title + if (a.y.options.title && a.y.used){ + style.halign = 'r'; + style.valign = 'm'; + style.angle = Flotr.toRad(a.y.options.titleAngle); + ctx.drawText( + a.y.options.title, + this.plotOffset.left - a.y.maxLabel.width - 2 * margin, + this.plotOffset.top + this.plotHeight / 2, + style + ); + } + + // Add y2 axis title + if (a.y2.options.title && a.y2.used){ + style.halign = 'l'; + style.valign = 'm'; + style.angle = Flotr.toRad(a.y2.options.titleAngle); + ctx.drawText( + a.y2.options.title, + this.plotOffset.left + this.plotWidth + a.y2.maxLabel.width + 2 * margin, + this.plotOffset.top + this.plotHeight / 2, + style + ); + } + } + else { + html = ['<div style="color:'+options.grid.color+';" class="flotr-titles">']; + + // Add title + if (options.title){ + html.push('<div style="position:absolute;top:0;left:'+this.plotOffset.left+'px;font-size:1em;font-weight:bold;text-align:center;width:'+this.plotWidth+'px;" class="flotr-title">'+options.title+'</div>'); + } + + // Add subtitle + if (options.subtitle){ + html.push('<div style="position:absolute;top:'+this.titleHeight+'px;left:'+this.plotOffset.left+'px;font-size:smaller;text-align:center;width:'+this.plotWidth+'px;" class="flotr-subtitle">'+options.subtitle+'</div>'); + } + html.push('</div>'); + + + html.push('<div class="flotr-axis-title" style="font-weight:bold;">'); + // Add x axis title + if (a.x.options.title && a.x.used){ + html.push('<div style="position:absolute;top:' + (this.plotOffset.top + this.plotHeight + options.grid.labelMargin + a.x.titleSize.height) + 'px;left:' + this.plotOffset.left + 'px;width:' + this.plotWidth + 'px;text-align:center;" class="flotr-axis-title">' + a.x.options.title + '</div>'); + } + + // Add x2 axis title + if (a.x2.options.title && a.x2.used){ + html.push('<div style="position:absolute;top:0;left:' + this.plotOffset.left + 'px;width:' + this.plotWidth + 'px;text-align:center;" class="flotr-axis-title">' + a.x2.options.title + '</div>'); + } + + // Add y axis title + if (a.y.options.title && a.y.used){ + html.push('<div style="position:absolute;top:' + (this.plotOffset.top + this.plotHeight/2 - a.y.titleSize.height/2) + 'px;left:0;text-align:right;" class="flotr-axis-title">' + a.y.options.title + '</div>'); + } + + // Add y2 axis title + if (a.y2.options.title && a.y2.used){ + html.push('<div style="position:absolute;top:' + (this.plotOffset.top + this.plotHeight/2 - a.y.titleSize.height/2) + 'px;right:0;text-align:right;" class="flotr-axis-title">' + a.y2.options.title + '</div>'); + } + html.push('</div>'); + + this.el.insert(html.join('')); + } + }, + /** + * Actually draws the graph. + * @param {Object} series - series to draw + */ + drawSeries: function(series){ + series = series || this.series; + + var drawn = false; + for(var type in Flotr._registeredTypes){ + if(series[type] && series[type].show){ + this[Flotr._registeredTypes[type]](series); + drawn = true; + } + } + + if(!drawn){ + this[Flotr._registeredTypes[this.options.defaultType]](series); + } + }, + + plotLine: function(series, offset){ + var ctx = this.ctx, + xa = series.xaxis, + ya = series.yaxis, + tHoz = this.tHoz.bind(this), + tVert = this.tVert.bind(this), + data = series.data; + + if(data.length < 2) return; + + var prevx = tHoz(data[0][0], xa), + prevy = tVert(data[0][1], ya) + offset; + + ctx.beginPath(); + ctx.moveTo(prevx, prevy); + for(var i = 0; i < data.length - 1; ++i){ + var x1 = data[i][0], y1 = data[i][1], + x2 = data[i+1][0], y2 = data[i+1][1]; + + // To allow empty values + if (y1 === null || y2 === null) continue; + + /** + * Clip with ymin. + */ + if(y1 <= y2 && y1 < ya.min){ + /** + * Line segment is outside the drawing area. + */ + if(y2 < ya.min) continue; + + /** + * Compute new intersection point. + */ + x1 = (ya.min - y1) / (y2 - y1) * (x2 - x1) + x1; + y1 = ya.min; + }else if(y2 <= y1 && y2 < ya.min){ + if(y1 < ya.min) continue; + x2 = (ya.min - y1) / (y2 - y1) * (x2 - x1) + x1; + y2 = ya.min; + } + + /** + * Clip with ymax. + */ + if(y1 >= y2 && y1 > ya.max) { + if(y2 > ya.max) continue; + x1 = (ya.max - y1) / (y2 - y1) * (x2 - x1) + x1; + y1 = ya.max; + } + else if(y2 >= y1 && y2 > ya.max){ + if(y1 > ya.max) continue; + x2 = (ya.max - y1) / (y2 - y1) * (x2 - x1) + x1; + y2 = ya.max; + } + + /** + * Clip with xmin. + */ + if(x1 <= x2 && x1 < xa.min){ + if(x2 < xa.min) continue; + y1 = (xa.min - x1) / (x2 - x1) * (y2 - y1) + y1; + x1 = xa.min; + }else if(x2 <= x1 && x2 < xa.min){ + if(x1 < xa.min) continue; + y2 = (xa.min - x1) / (x2 - x1) * (y2 - y1) + y1; + x2 = xa.min; + } + + /** + * Clip with xmax. + */ + if(x1 >= x2 && x1 > xa.max){ + if (x2 > xa.max) continue; + y1 = (xa.max - x1) / (x2 - x1) * (y2 - y1) + y1; + x1 = xa.max; + }else if(x2 >= x1 && x2 > xa.max){ + if(x1 > xa.max) continue; + y2 = (xa.max - x1) / (x2 - x1) * (y2 - y1) + y1; + x2 = xa.max; + } + + if(prevx != tHoz(x1, xa) || prevy != tVert(y1, ya) + offset) + ctx.moveTo(tHoz(x1, xa), tVert(y1, ya) + offset); + + prevx = tHoz(x2, xa); + prevy = tVert(y2, ya) + offset; + ctx.lineTo(prevx, prevy); + } + ctx.stroke(); + }, + /** + * Function used to fill + * @param {Object} data + */ + plotLineArea: function(series, offset){ + var data = series.data; + if(data.length < 2) return; + + var top, lastX = 0, + ctx = this.ctx, + xa = series.xaxis, + ya = series.yaxis, + tHoz = this.tHoz.bind(this), + tVert = this.tVert.bind(this), + bottom = Math.min(Math.max(0, ya.min), ya.max), + first = true; + + ctx.beginPath(); + for(var i = 0; i < data.length - 1; ++i){ + + var x1 = data[i][0], y1 = data[i][1], + x2 = data[i+1][0], y2 = data[i+1][1]; + + if(x1 <= x2 && x1 < xa.min){ + if(x2 < xa.min) continue; + y1 = (xa.min - x1) / (x2 - x1) * (y2 - y1) + y1; + x1 = xa.min; + }else if(x2 <= x1 && x2 < xa.min){ + if(x1 < xa.min) continue; + y2 = (xa.min - x1) / (x2 - x1) * (y2 - y1) + y1; + x2 = xa.min; + } + + if(x1 >= x2 && x1 > xa.max){ + if(x2 > xa.max) continue; + y1 = (xa.max - x1) / (x2 - x1) * (y2 - y1) + y1; + x1 = xa.max; + }else if(x2 >= x1 && x2 > xa.max){ + if (x1 > xa.max) continue; + y2 = (xa.max - x1) / (x2 - x1) * (y2 - y1) + y1; + x2 = xa.max; + } + + if(first){ + ctx.moveTo(tHoz(x1, xa), tVert(bottom, ya) + offset); + first = false; + } + + /** + * Now check the case where both is outside. + */ + if(y1 >= ya.max && y2 >= ya.max){ + ctx.lineTo(tHoz(x1, xa), tVert(ya.max, ya) + offset); + ctx.lineTo(tHoz(x2, xa), tVert(ya.max, ya) + offset); + continue; + }else if(y1 <= ya.min && y2 <= ya.min){ + ctx.lineTo(tHoz(x1, xa), tVert(ya.min, ya) + offset); + ctx.lineTo(tHoz(x2, xa), tVert(ya.min, ya) + offset); + 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 < ya.min && y2 >= ya.min){ + x1 = (ya.min - y1) / (y2 - y1) * (x2 - x1) + x1; + y1 = ya.min; + }else if(y2 <= y1 && y2 < ya.min && y1 >= ya.min){ + x2 = (ya.min - y1) / (y2 - y1) * (x2 - x1) + x1; + y2 = ya.min; + } + + /** + * Clip with ymax. + */ + if(y1 >= y2 && y1 > ya.max && y2 <= ya.max){ + x1 = (ya.max - y1) / (y2 - y1) * (x2 - x1) + x1; + y1 = ya.max; + }else if(y2 >= y1 && y2 > ya.max && y1 <= ya.max){ + x2 = (ya.max - y1) / (y2 - y1) * (x2 - x1) + x1; + y2 = ya.max; + } + + /** + * If the x value was changed we got a rectangle to fill. + */ + if(x1 != x1old){ + top = (y1 <= ya.min) ? top = ya.min : ya.max; + ctx.lineTo(tHoz(x1old, xa), tVert(top, ya) + offset); + ctx.lineTo(tHoz(x1, xa), tVert(top, ya) + offset); + } + + /** + * Fill the triangles. + */ + ctx.lineTo(tHoz(x1, xa), tVert(y1, ya) + offset); + ctx.lineTo(tHoz(x2, xa), tVert(y2, ya) + offset); + + /** + * Fill the other rectangle if it's there. + */ + if(x2 != x2old){ + top = (y2 <= ya.min) ? ya.min : ya.max; + ctx.lineTo(tHoz(x2old, xa), tVert(top, ya) + offset); + ctx.lineTo(tHoz(x2, xa), tVert(top, ya) + offset); + } + + lastX = Math.max(x2, x2old); + } + + ctx.lineTo(tHoz(lastX, xa), tVert(bottom, ya) + offset); + ctx.closePath(); + ctx.fill(); + }, + /** + * Function: (private) drawSeriesLines + * + * Function draws lines series in the canvas element. + * + * Parameters: + * series - Series with options.lines.show = true. + * + * Returns: + * void + */ + drawSeriesLines: function(series){ + series = series || this.series; + var ctx = this.ctx; + ctx.save(); + ctx.translate(this.plotOffset.left, this.plotOffset.top); + ctx.lineJoin = 'round'; + + var lw = series.lines.lineWidth; + var sw = series.shadowSize; + + if(sw > 0){ + ctx.lineWidth = sw / 2; + + var offset = lw/2 + ctx.lineWidth/2; + + ctx.strokeStyle = "rgba(0,0,0,0.1)"; + this.plotLine(series, offset + sw/2); + + ctx.strokeStyle = "rgba(0,0,0,0.2)"; + this.plotLine(series, offset); + + if(series.lines.fill) { + ctx.fillStyle = "rgba(0,0,0,0.05)"; + this.plotLineArea(series, offset + sw/2); + } + } + + ctx.lineWidth = lw; + ctx.strokeStyle = series.color; + if(series.lines.fill){ + ctx.fillStyle = series.lines.fillColor != null ? series.lines.fillColor : Flotr.parseColor(series.color).scale(null, null, null, series.lines.fillOpacity).toString(); + this.plotLineArea(series, 0); + } + + this.plotLine(series, 0); + ctx.restore(); + }, + /** + * Function: drawSeriesPoints + * + * Function draws point series in the canvas element. + * + * Parameters: + * series - Series with options.points.show = true. + * + * Returns: + * void + */ + drawSeriesPoints: function(series) { + var ctx = this.ctx; + + ctx.save(); + ctx.translate(this.plotOffset.left, this.plotOffset.top); + + var lw = series.lines.lineWidth; + var sw = series.shadowSize; + + if(sw > 0){ + ctx.lineWidth = sw / 2; + + ctx.strokeStyle = 'rgba(0,0,0,0.1)'; + this.plotPointShadows(series, sw/2 + ctx.lineWidth/2, series.points.radius); + + ctx.strokeStyle = 'rgba(0,0,0,0.2)'; + this.plotPointShadows(series, ctx.lineWidth/2, series.points.radius); + } + + ctx.lineWidth = series.points.lineWidth; + ctx.strokeStyle = series.color; + ctx.fillStyle = series.points.fillColor != null ? series.points.fillColor : series.color; + this.plotPoints(series, series.points.radius, series.points.fill); + ctx.restore(); + }, + plotPoints: function (series, radius, fill) { + var xa = series.xaxis, + ya = series.yaxis, + ctx = this.ctx, i, + data = series.data; + + for(i = data.length - 1; i > -1; --i){ + var x = data[i][0], y = data[i][1]; + if(x < xa.min || x > xa.max || y < ya.min || y > ya.max) + continue; + + ctx.beginPath(); + ctx.arc(this.tHoz(x, xa), this.tVert(y, ya), radius, 0, 2 * Math.PI, true); + if(fill) ctx.fill(); + ctx.stroke(); + } + }, + plotPointShadows: function(series, offset, radius){ + var xa = series.xaxis, + ya = series.yaxis, + ctx = this.ctx, i, + data = series.data; + + for(i = data.length - 1; i > -1; --i){ + var x = data[i][0], y = data[i][1]; + if (x < xa.min || x > xa.max || y < ya.min || y > ya.max) + continue; + ctx.beginPath(); + ctx.arc(this.tHoz(x, xa), this.tVert(y, ya) + offset, radius, 0, Math.PI, false); + ctx.stroke(); + } + }, + /** + * Function: drawSeriesBars + * + * Function draws bar series in the canvas element. + * + * Parameters: + * series - Series with options.bars.show = true. + * + * Returns: + * void + */ + drawSeriesBars: function(series) { + var ctx = this.ctx, + bw = series.bars.barWidth, + lw = Math.min(series.bars.lineWidth, bw); + + ctx.save(); + ctx.translate(this.plotOffset.left, this.plotOffset.top); + ctx.lineJoin = 'miter'; + + /** + * @todo linewidth not interpreted the right way. + */ + ctx.lineWidth = lw; + ctx.strokeStyle = series.color; + + this.plotBarsShadows(series, bw, 0, series.bars.fill); + + if(series.bars.fill){ + ctx.fillStyle = series.bars.fillColor != null ? series.bars.fillColor : Flotr.parseColor(series.color).scale(null, null, null, series.bars.fillOpacity).toString(); + } + + this.plotBars(series, bw, 0, series.bars.fill); + ctx.restore(); + }, + plotBars: function(series, barWidth, offset, fill){ + var data = series.data; + if(data.length < 1) return; + + var xa = series.xaxis, + ya = series.yaxis, + ctx = this.ctx, + tHoz = this.tHoz.bind(this), + tVert = this.tVert.bind(this); + + for(var i = 0; i < data.length; i++){ + var x = data[i][0], + y = data[i][1]; + var drawLeft = true, drawTop = true, drawRight = true; + + // Stacked bars + var stackOffset = 0; + if(series.bars.stacked) { + xa.values.each(function(o, v) { + if (v == x) { + stackOffset = o.stack || 0; + o.stack = stackOffset + y; + } + }); + } + + // @todo: fix horizontal bars support + // Horizontal bars + if(series.bars.horizontal) + var left = stackOffset, right = x + stackOffset, bottom = y, top = y + barWidth; + else + var left = x, right = x + barWidth, bottom = stackOffset, top = y + stackOffset; + + if(right < xa.min || left > xa.max || top < ya.min || bottom > ya.max) + continue; + + if(left < xa.min){ + left = xa.min; + drawLeft = false; + } + + if(right > xa.max){ + right = xa.max; + if (xa.lastSerie != series && series.bars.horizontal) + drawTop = false; + } + + if(bottom < ya.min) + bottom = ya.min; + + if(top > ya.max){ + top = ya.max; + if (ya.lastSerie != series && !series.bars.horizontal) + drawTop = false; + } + + /** + * Fill the bar. + */ + if(fill){ + ctx.beginPath(); + ctx.moveTo(tHoz(left, xa), tVert(bottom, ya) + offset); + ctx.lineTo(tHoz(left, xa), tVert(top, ya) + offset); + ctx.lineTo(tHoz(right, xa), tVert(top, ya) + offset); + ctx.lineTo(tHoz(right, xa), tVert(bottom, ya) + offset); + ctx.fill(); + } + + /** + * Draw bar outline/border. + */ + if(series.bars.lineWidth != 0 && (drawLeft || drawRight || drawTop)){ + ctx.beginPath(); + ctx.moveTo(tHoz(left, xa), tVert(bottom, ya) + offset); + + ctx[drawLeft ?'lineTo':'moveTo'](tHoz(left, xa), tVert(top, ya) + offset); + ctx[drawTop ?'lineTo':'moveTo'](tHoz(right, xa), tVert(top, ya) + offset); + ctx[drawRight?'lineTo':'moveTo'](tHoz(right, xa), tVert(bottom, ya) + offset); + + ctx.stroke(); + } + } + }, + plotBarsShadows: function(series, barWidth, offset){ + var data = series.data; + if(data.length < 1) return; + + var xa = series.xaxis, + ya = series.yaxis, + ctx = this.ctx, + tHoz = this.tHoz.bind(this), + tVert = this.tVert.bind(this), + sw = this.options.shadowSize; + + for(var i = 0; i < data.length; i++){ + var x = data[i][0], + y = data[i][1]; + + // Stacked bars + var stackOffset = 0; + if(series.bars.stacked) { + xa.values.each(function(o, v) { + if (v == x) { + stackOffset = o.stackShadow || 0; + o.stackShadow = stackOffset + y; + } + }); + } + + // Horizontal bars + if(series.bars.horizontal) + var left = stackOffset, right = x + stackOffset, bottom = y, top = y + barWidth; + else + var left = x, right = x + barWidth, bottom = stackOffset, top = y + stackOffset; + + if(right < xa.min || left > xa.max || top < ya.min || bottom > ya.max) + continue; + + if(left < xa.min) left = xa.min; + if(right > xa.max) right = xa.max; + if(bottom < ya.min) bottom = ya.min; + if(top > ya.max) top = ya.max; + + var width = tHoz(right, xa)-tHoz(left, xa)-((tHoz(right, xa)+sw <= this.plotWidth) ? 0 : sw); + var height = Math.max(0, tVert(bottom, ya)-tVert(top, ya)-((tVert(bottom, ya)+sw <= this.plotHeight) ? 0 : sw)); + + ctx.fillStyle = 'rgba(0,0,0,0.05)'; + ctx.fillRect(Math.min(tHoz(left, xa)+sw, this.plotWidth), Math.min(tVert(top, ya)+sw, this.plotWidth), width, height); + } + }, + /** + * Function: drawSeriesCandles + * + * Function draws candles series in the canvas element. + * + * Parameters: + * series - Series with options.candles.show = true. + * + * Returns: + * void + */ + drawSeriesCandles: function(series) { + var ctx = this.ctx, + bw = series.candles.candleWidth; + + ctx.save(); + ctx.translate(this.plotOffset.left, this.plotOffset.top); + ctx.lineJoin = 'miter'; + + /** + * @todo linewidth not interpreted the right way. + */ + ctx.lineWidth = series.candles.lineWidth; + this.plotCandlesShadows(series, bw/2); + this.plotCandles(series, bw/2); + + ctx.restore(); + }, + plotCandles: function(series, offset){ + var data = series.data; + if(data.length < 1) return; + + var xa = series.xaxis, + ya = series.yaxis, + ctx = this.ctx, + tHoz = this.tHoz.bind(this), + tVert = this.tVert.bind(this); + + for(var i = 0; i < data.length; i++){ + var d = data[i], + x = d[0], + open = d[1], + high = d[2], + low = d[3], + close = d[4]; + + var left = x, + right = x + series.candles.candleWidth, + bottom = Math.max(ya.min, low), + top = Math.min(ya.max, high), + bottom2 = Math.max(ya.min, Math.min(open, close)), + top2 = Math.min(ya.max, Math.max(open, close)); + + if(right < xa.min || left > xa.max || top < ya.min || bottom > ya.max) + continue; + + var color = series.candles[open>close?'downFillColor':'upFillColor']; + /** + * Fill the candle. + */ + if(series.candles.fill && !series.candles.barcharts){ + ctx.fillStyle = Flotr.parseColor(color).scale(null, null, null, series.candles.fillOpacity).toString(); + ctx.fillRect(tHoz(left, xa), tVert(top2, ya) + offset, tHoz(right, xa) - tHoz(left, xa), tVert(bottom2, ya) - tVert(top2, ya)); + } + + /** + * Draw candle outline/border, high, low. + */ + if(series.candles.lineWidth || series.candles.wickLineWidth){ + var x, y, pixelOffset = (series.candles.wickLineWidth % 2) / 2; + + x = Math.floor(tHoz((left + right) / 2), xa) + pixelOffset; + + ctx.save(); + ctx.strokeStyle = color; + ctx.lineWidth = series.candles.wickLineWidth; + ctx.lineCap = 'butt'; + + if (series.candles.barcharts) { + ctx.beginPath(); + + ctx.moveTo(x, Math.floor(tVert(top, ya) + offset)); + ctx.lineTo(x, Math.floor(tVert(bottom, ya) + offset)); + + y = Math.floor(tVert(open, ya) + offset)+0.5; + ctx.moveTo(Math.floor(tHoz(left, xa))+pixelOffset, y); + ctx.lineTo(x, y); + + y = Math.floor(tVert(close, ya) + offset)+0.5; + ctx.moveTo(Math.floor(tHoz(right, xa))+pixelOffset, y); + ctx.lineTo(x, y); + } + else { + ctx.strokeRect(tHoz(left, xa), tVert(top2, ya) + offset, tHoz(right, xa) - tHoz(left, xa), tVert(bottom2, ya) - tVert(top2, ya)); + + ctx.beginPath(); + ctx.moveTo(x, Math.floor(tVert(top2, ya) + offset)); + ctx.lineTo(x, Math.floor(tVert(top, ya) + offset)); + ctx.moveTo(x, Math.floor(tVert(bottom2, ya) + offset)); + ctx.lineTo(x, Math.floor(tVert(bottom, ya) + offset)); + } + + ctx.stroke(); + ctx.restore(); + } + } + }, + plotCandlesShadows: function(series, offset){ + var data = series.data; + if(data.length < 1 || series.candles.barcharts) return; + + var xa = series.xaxis, + ya = series.yaxis, + tHoz = this.tHoz.bind(this), + tVert = this.tVert.bind(this), + sw = this.options.shadowSize; + + for(var i = 0; i < data.length; i++){ + var d = data[i], + x = d[0], + open = d[1], + high = d[2], + low = d[3], + close = d[4]; + + var left = x, + right = x + series.candles.candleWidth, + bottom = Math.max(ya.min, Math.min(open, close)), + top = Math.min(ya.max, Math.max(open, close)); + + if(right < xa.min || left > xa.max || top < ya.min || bottom > ya.max) + continue; + + var width = tHoz(right, xa)-tHoz(left, xa)-((tHoz(right, xa)+sw <= this.plotWidth) ? 0 : sw); + var height = Math.max(0, tVert(bottom, ya)-tVert(top, ya)-((tVert(bottom, ya)+sw <= this.plotHeight) ? 0 : sw)); + + this.ctx.fillStyle = 'rgba(0,0,0,0.05)'; + this.ctx.fillRect(Math.min(tHoz(left, xa)+sw, this.plotWidth), Math.min(tVert(top, ya)+sw, this.plotWidth), width, height); + } + }, + /** + * Function: drawSeriesPie + * + * Function draws a pie in the canvas element. + * + * Parameters: + * series - Series with options.pie.show = true. + * + * Returns: + * void + */ + drawSeriesPie: function(series) { + if (!this.options.pie.drawn) { + var ctx = this.ctx, + options = this.options, + lw = series.pie.lineWidth, + sw = series.shadowSize, + data = series.data, + radius = (Math.min(this.canvasWidth, this.canvasHeight) * series.pie.sizeRatio) / 2, + html = []; + + var vScale = 1;//Math.cos(series.pie.viewAngle); + var plotTickness = Math.sin(series.pie.viewAngle)*series.pie.spliceThickness / vScale; + + var style = { + size: options.fontSize*1.2, + color: options.grid.color, + weight: 1.5 + }; + + var center = { + x: (this.canvasWidth+this.plotOffset.left)/2, + y: (this.canvasHeight-this.plotOffset.bottom)/2 + }; + + // Pie portions + var portions = this.series.collect(function(hash, index){ + if (hash.pie.show) + return { + name: (hash.label || hash.data[0][1]), + value: [index, hash.data[0][1]], + explode: hash.pie.explode + }; + }); + + // Sum of the portions' angles + var sum = portions.pluck('value').pluck(1).inject(0, function(acc, n) { return acc + n; }); + + var fraction = 0.0, + angle = series.pie.startAngle, + value = 0.0; + + var slices = portions.collect(function(slice){ + angle += fraction; + value = parseFloat(slice.value[1]); // @warning : won't support null values !! + fraction = value/sum; + return { + name: slice.name, + fraction: fraction, + x: slice.value[0], + y: value, + explode: slice.explode, + startAngle: 2 * angle * Math.PI, + endAngle: 2 * (angle + fraction) * Math.PI + }; + }); + + ctx.save(); + + if(sw > 0){ + slices.each(function (slice) { + var bisection = (slice.startAngle + slice.endAngle) / 2; + + var xOffset = center.x + Math.cos(bisection) * slice.explode + sw; + var yOffset = center.y + Math.sin(bisection) * slice.explode + sw; + + this.plotSlice(xOffset, yOffset, radius, slice.startAngle, slice.endAngle, false, vScale); + + ctx.fillStyle = 'rgba(0,0,0,0.1)'; + ctx.fill(); + }, this); + } + + if (options.HtmlText) { + html = ['<div style="color:' + this.options.grid.color + '" class="flotr-labels">']; + } + + slices.each(function (slice, index) { + var bisection = (slice.startAngle + slice.endAngle) / 2; + var color = options.colors[index]; + + var xOffset = center.x + Math.cos(bisection) * slice.explode; + var yOffset = center.y + Math.sin(bisection) * slice.explode; + + this.plotSlice(xOffset, yOffset, radius, slice.startAngle, slice.endAngle, false, vScale); + + if(series.pie.fill){ + ctx.fillStyle = Flotr.parseColor(color).scale(null, null, null, series.pie.fillOpacity).toString(); + ctx.fill(); + } + ctx.lineWidth = lw; + ctx.strokeStyle = color; + ctx.stroke(); + + /*ctx.save(); + ctx.scale(1, vScale); + + ctx.moveTo(xOffset, yOffset); + ctx.beginPath(); + ctx.lineTo(xOffset, yOffset+plotTickness); + ctx.lineTo(xOffset+Math.cos(slice.startAngle)*radius, yOffset+Math.sin(slice.startAngle)*radius+plotTickness); + ctx.lineTo(xOffset+Math.cos(slice.startAngle)*radius, yOffset+Math.sin(slice.startAngle)*radius); + ctx.lineTo(xOffset, yOffset); + ctx.closePath(); + ctx.fill();ctx.stroke(); + + ctx.moveTo(xOffset, yOffset); + ctx.beginPath(); + ctx.lineTo(xOffset, yOffset+plotTickness); + ctx.lineTo(xOffset+Math.cos(slice.endAngle)*radius, yOffset+Math.sin(slice.endAngle)*radius+plotTickness); + ctx.lineTo(xOffset+Math.cos(slice.endAngle)*radius, yOffset+Math.sin(slice.endAngle)*radius); + ctx.lineTo(xOffset, yOffset); + ctx.closePath(); + ctx.fill();ctx.stroke(); + + ctx.moveTo(xOffset+Math.cos(slice.startAngle)*radius, yOffset+Math.sin(slice.startAngle)*radius); + ctx.beginPath(); + ctx.lineTo(xOffset+Math.cos(slice.startAngle)*radius, yOffset+Math.sin(slice.startAngle)*radius+plotTickness); + ctx.arc(xOffset, yOffset+plotTickness, radius, slice.startAngle, slice.endAngle, false); + ctx.lineTo(xOffset+Math.cos(slice.endAngle)*radius, yOffset+Math.sin(slice.endAngle)*radius); + ctx.arc(xOffset, yOffset, radius, slice.endAngle, slice.startAngle, true); + ctx.closePath(); + ctx.fill();ctx.stroke(); + + ctx.scale(1, 1/vScale); + this.plotSlice(xOffset, yOffset+plotTickness, radius, slice.startAngle, slice.endAngle, false, vScale); + ctx.stroke(); + if(series.pie.fill){ + ctx.fillStyle = Flotr.parseColor(color).scale(null, null, null, series.pie.fillOpacity).toString(); + ctx.fill(); + } + + ctx.restore();*/ + + var label = options.pie.labelFormatter(slice); + + var textAlignRight = (Math.cos(bisection) < 0); + var distX = xOffset + Math.cos(bisection) * (series.pie.explode + radius); + var distY = yOffset + Math.sin(bisection) * (series.pie.explode + radius); + + if (slice.fraction && label) { + if (options.HtmlText) { + var divStyle = 'position:absolute;top:' + (distY - 5) + 'px;'; //@todo: change + if (textAlignRight) { + divStyle += 'right:'+(this.canvasWidth - distX)+'px;text-align:right;'; + } + else { + divStyle += 'left:'+distX+'px;text-align:left;'; + } + html.push('<div style="' + divStyle + '" class="flotr-grid-label">' + label + '</div>'); + } + else { + style.halign = textAlignRight ? 'r' : 'l'; + ctx.drawText( + label, + distX, + distY + style.size / 2, + style + ); + } + } + }, this); + + if (options.HtmlText) { + html.push('</div>'); + this.el.insert(html.join('')); + } + + ctx.restore(); + options.pie.drawn = true; + } + }, + plotSlice: function(x, y, radius, startAngle, endAngle, fill, vScale) { + var ctx = this.ctx; + vScale = vScale || 1; + + ctx.save(); + ctx.scale(1, vScale); + ctx.beginPath(); + ctx.moveTo(x, y); + ctx.arc (x, y, radius, startAngle, endAngle, fill); + ctx.lineTo(x, y); + ctx.closePath(); + ctx.restore(); + }, + plotPie: function() {}, + /** + * Function: insertLegend + * + * Function adds a legend div to the canvas container or draws it on the canvas. + * + * Parameters: + * none + * + * Returns: + * void + */ + insertLegend: function(){ + if(!this.options.legend.show) + return; + + var series = this.series, + plotOffset = this.plotOffset, + options = this.options, + fragments = [], + rowStarted = false, + ctx = this.ctx, + i; + + var noLegendItems = series.findAll(function(s) {return (s.label && !s.hide)}).size(); + + if (noLegendItems) { + if (!options.HtmlText && this.textEnabled) { + var style = { + size: options.fontSize*1.1, + color: options.grid.color + }; + + // @todo: take css into account + //var dummyDiv = this.el.insert('<div class="flotr-legend" style="position:absolute;top:-10000px;"></div>'); + + var p = options.legend.position, + m = options.legend.margin, + lbw = options.legend.labelBoxWidth, + lbh = options.legend.labelBoxHeight, + lbm = options.legend.labelBoxMargin, + offsetX = plotOffset.left + m, + offsetY = plotOffset.top + m; + + // We calculate the labels' max width + var labelMaxWidth = 0; + for(i = series.length - 1; i > -1; --i){ + if(!series[i].label || series[i].hide) continue; + var label = options.legend.labelFormatter(series[i].label); + labelMaxWidth = Math.max(labelMaxWidth, ctx.measureText(label, style)); + } + + var legendWidth = Math.round(lbw + lbm*3 + labelMaxWidth), + legendHeight = Math.round(noLegendItems*(lbm+lbh) + lbm); + + if(p.charAt(0) == 's') offsetY = plotOffset.top + this.plotHeight - (m + legendHeight); + if(p.charAt(1) == 'e') offsetX = plotOffset.left + this.plotWidth - (m + legendWidth); + + // Legend box + var color = Flotr.parseColor(options.legend.backgroundColor || 'rgb(240,240,240)').scale(null, null, null, options.legend.backgroundOpacity || 0.1).toString(); + + ctx.fillStyle = color; + ctx.fillRect(offsetX, offsetY, legendWidth, legendHeight); + ctx.strokeStyle = options.legend.labelBoxBorderColor; + ctx.strokeRect(Flotr.toPixel(offsetX), Flotr.toPixel(offsetY), legendWidth, legendHeight); + + // Legend labels + var x = offsetX + lbm; + var y = offsetY + lbm; + for(i = 0; i < series.length; i++){ + if(!series[i].label || series[i].hide) continue; + var label = options.legend.labelFormatter(series[i].label); + + ctx.fillStyle = series[i].color; + ctx.fillRect(x, y, lbw-1, lbh-1); + + ctx.strokeStyle = options.legend.labelBoxBorderColor; + ctx.lineWidth = 1; + ctx.strokeRect(Math.ceil(x)-1.5, Math.ceil(y)-1.5, lbw+2, lbh+2); + + // Legend text + ctx.drawText( + label, + x + lbw + lbm, + y + (lbh + style.size - ctx.fontDescent(style))/2, + style + ); + + y += lbh + lbm; + } + } + else { + for(i = 0; i < series.length; ++i){ + if(!series[i].label || series[i].hide) continue; + + if(i % options.legend.noColumns == 0){ + fragments.push(rowStarted ? '</tr><tr>' : '<tr>'); + rowStarted = true; + } + + var label = options.legend.labelFormatter(series[i].label); + + fragments.push('<td class="flotr-legend-color-box"><div style="border:1px solid ' + options.legend.labelBoxBorderColor + ';padding:1px"><div style="width:' + options.legend.labelBoxWidth + 'px;height:' + options.legend.labelBoxHeight + 'px;background-color:' + series[i].color + '"></div></div></td>' + + '<td class="flotr-legend-label">' + label + '</td>'); + } + if(rowStarted) fragments.push('</tr>'); + + if(fragments.length > 0){ + var table = '<table style="font-size:smaller;color:' + options.grid.color + '">' + fragments.join("") + '</table>'; + if(options.legend.container != null){ + $(options.legend.container).update(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) + 'px;'; + if(p.charAt(1) == 'e') pos += 'right:' + (m + plotOffset.right) + 'px;'; + else if(p.charAt(1) == 'w') pos += 'left:' + (m + plotOffset.left) + 'px;'; + + var div = this.el.insert('<div class="flotr-legend" style="position:absolute;z-index:2;' + pos +'">' + table + '</div>').select('div.flotr-legend').first(); + + 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 == null){ + var tmp = (options.grid.backgroundColor != null) ? options.grid.backgroundColor : Flotr.extractColor(div); + c = Flotr.parseColor(tmp).adjust(null, null, null, 1).toString(); + } + this.el.insert('<div class="flotr-legend-bg" style="position:absolute;width:' + div.getWidth() + 'px;height:' + div.getHeight() + 'px;' + pos +'background-color:' + c + ';"> </div>').select('div.flotr-legend-bg').first().setStyle({ + 'opacity': options.legend.backgroundOpacity + }); + } + } + } + } + } + }, + /** + * Function: getEventPosition + * + * Calculates the coordinates from a mouse event object. + * + * Parameters: + * event - Mouse Event object. + * + * Returns: + * Object with x and y coordinates of the mouse. + */ + getEventPosition: function (event){ + var offset = this.overlay.cumulativeOffset(), + rx = (event.pageX - offset.left - this.plotOffset.left), + ry = (event.pageY - offset.top - this.plotOffset.top), + ax = 0, ay = 0 + + if(event.pageX == null && event.clientX != null){ + var de = document.documentElement, b = document.body; + ax = event.clientX + (de && de.scrollLeft || b.scrollLeft || 0); + ay = event.clientY + (de && de.scrollTop || b.scrollTop || 0); + }else{ + ax = event.pageX; + ay = event.pageY; + } + + return { + x: this.axes.x.min + rx / this.axes.x.scale, + x2: this.axes.x2.min + rx / this.axes.x2.scale, + y: this.axes.y.max - ry / this.axes.y.scale, + y2: this.axes.y2.max - ry / this.axes.y2.scale, + relX: rx, + relY: ry, + absX: ax, + absY: ay + }; + }, + /** + * Function: clickHandler + * + * Handler observes the 'click' event and fires the 'flotr:click' event. + * + * Parameters: + * event - 'click' Event object. + * + * Returns: + * void + */ + clickHandler: function(event){ + if(this.ignoreClick){ + this.ignoreClick = false; + return; + } + this.el.fire('flotr:click', [this.getEventPosition(event), this]); + }, + /** + * Function: mouseMoveHandler + * + * Handler observes mouse movement over the graph area. Fires the + * 'flotr:mousemove' event. + * + * Parameters: + * event - 'mousemove' Event object. + * + * Returns: + * void + */ + mouseMoveHandler: function(event){ + var pos = this.getEventPosition(event); + + this.lastMousePos.pageX = pos.absX; + this.lastMousePos.pageY = pos.absY; + if(this.selectionInterval == null && (this.options.mouse.track || this.series.any(function(s){return s.mouse && s.mouse.track;}))){ + this.hit(pos); + } + + this.el.fire('flotr:mousemove', [event, pos, this]); + }, + /** + * Function: mouseDownHandler + * + * Handler observes the 'mousedown' event. + * + * Parameters: + * event - 'mousedown' Event object. + * + * Returns: + * void + */ + mouseDownHandler: function (event){ + if(event.isRightClick()) { + event.stop(); + var overlay = this.overlay; + overlay.hide(); + + function cancelContextMenu () { + overlay.show(); + $(document).stopObserving('mousemove', cancelContextMenu); [... 567 lines stripped ...] |
Free forum by Nabble | Edit this page |