//Flotr 0.2.0-alpha Copyright (c) 2009 Bas Wenneker, , MIT License. // //Radar chart added by Ryan Simmons // /* $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', 'radar':'drawSeriesRadar' }, /** * 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 }, radar: { show: false, // => setting to true will show radar chart, 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 }, radarChartMode: false, // => true to render radar grid / and setup scaling for radar chart 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.radar = Object.extend(Object.clone(this.options.radar), s.radar); 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('
' + text + '
').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 = ['']; // First row : series' labels var html = ['']; html.push(' '); for (i = 0; i < s.length; ++i) { html.push(''+(s[i].label || String.fromCharCode(65+i))+''); colgroup.push(''); } html.push(''); // Data rows for (j = 0; j < datagrid.length; ++j) { html.push(''); 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+''); } html.push(''); } colgroup.push(''); 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; } } } if (this.options.radarChartMode) { xaxis.datamin = yaxis.datamin = - yaxis.datamax; xaxis.datamax = yaxis.datamax; if (!this.options.radarChartSides) this.options.radarChartSides = data.length; } } } 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, ((this.options.radarChartMode) ? 0 : 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 + this.options.radarChartMode ? (y.options.showLabels ? (y.maxLabel.height + margin) : 0) : 0; 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) + this.options.radarChartMode ? (x.options.showLabels ? (x.maxLabel.width + margin) : 0) : 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(){ if (this.options.radarChartMode) { // If we are in radar chart mode call drawRadarGrid instead and exit this.drawRadarGrid(); return; } 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 a grid for the graph. */ drawRadarGrid: function(){ var v, o = this.options, ctx = this.ctx; var sides = this.options.radarChartSides, degreesInRadiansForAngle = Math.PI * 2 / sides, nintyDegrees = Math.PI / 2; 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); ctx.lineJoin = 'round'; // 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 var regPoly = {}; regPoly.xaxis = {}; regPoly.yaxis = {}; regPoly.xaxis.min = regPoly.yaxis.min = this.axes.x.min; regPoly.xaxis.max = regPoly.yaxis.max = this.axes.x.max; regPoly.xaxis.scale = this.plotWidth / (this.axes.x.max - this.axes.x.min); regPoly.yaxis.scale = this.plotHeight / (this.axes.x.max - this.axes.x.min); ctx.lineWidth = 1; ctx.strokeStyle = o.grid.tickColor; if(o.grid.horizontalLines){ for(var j = 0; j < this.axes.y.ticks.length; ++j){ v = this.axes.y.ticks[j].v; if (v < 0) continue; // 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; regPoly.data = new Array(); for (i = 0; i < sides; i++) { angle = nintyDegrees + (degreesInRadiansForAngle * i); regPoly.data[i] = [v * Math.cos(angle), v * Math.sin(angle)] } regPoly.data[sides] = regPoly.data[0]; this.plotLine(regPoly,0); } } // Draw axis/grid border. if(o.grid.outlineWidth != 0) { ctx.lineWidth = o.grid.outlineWidth; ctx.strokeStyle = o.grid.color; regPoly.data = new Array(); var radius = this.axes.x.max; for (i = 0; i < sides; i++) { angle = nintyDegrees + (degreesInRadiansForAngle * i); regPoly.data[i] = [radius * Math.cos(angle), radius * Math.sin(angle)] } regPoly.data[sides] = regPoly.data[0]; this.plotLine(regPoly,0); } ctx.lineWidth = 1; ctx.strokeStyle = o.grid.tickColor; ctx.beginPath(); if(o.grid.verticalLines){ for(var i = 0; i < sides; ++i){ ctx.moveTo(Math.floor(this.tHoz(0)) + ctx.lineWidth/2, Math.floor(this.tVert(0)) + ctx.lineWidth/2); ctx.lineTo(Math.floor(this.tHoz(regPoly.data[i][0])) + ctx.lineWidth/2, Math.floor(this.tVert(regPoly.data[i][1])) + ctx.lineWidth/2); } } ctx.stroke(); ctx.restore(); if(o.grid.verticalLines || o.grid.horizontalLines){ this.el.fire('flotr:aftergrid', [this.axes.x, this.axes.y, o, this]); } }, /** * Draws labels aroung radar chart */ drawRadarLabels:function(){ var ctx = this.ctx, options = this.options, axis = this.axes.x, tick, minY = 0, maxY = 0, xOffset, yOffset; var style = { size: options.fontSize, adjustAlign: true }; style.color = axis.options.color || options.grid.color; style.angle = Flotr.toRad(axis.options.labelsAngle); var radius = axis.max * 1, closeTo = axis.max * 0.1, sides = this.options.radarChartSides, degreesInRadiansForAngle = Math.PI * 2 / sides, nintyDegrees = Math.PI / 2, posdata = new Array(); for (i = 0; i < sides; i++) { angle = nintyDegrees + (degreesInRadiansForAngle * i); posdata[i] = [radius * Math.cos(angle), radius * Math.sin(angle)]; if (minY > posdata[i][1]) minY = posdata[i][1]; if (maxY < posdata[i][1]) maxY = posdata[i][1]; } for (i = 0; i < sides; i++) { tick = axis.ticks[i]; if(!tick.label || tick.label.length == 0) continue; yOffset = 0; if (posdata[i][0] > 0) { style.halign = 'l'; xOffset = options.grid.labelMargin; } else { style.halign = 'r'; xOffset = - options.grid.labelMargin; } style.valign = 'm'; if ((posdata[i][1] + closeTo) >= minY && (posdata[i][1] - closeTo) <= minY) { style.valign = 't' ; style.halign = 'c'; yOffset = options.grid.labelMargin; }; if (posdata[i][1] == maxY) { style.valign = 'b' ; style.halign = 'c'; yOffset = - options.grid.labelMargin; } ctx.drawText( tick.label, this.plotOffset.left + this.tHoz(posdata[i][0]) + xOffset, this.plotOffset.top + this.tVert(posdata[i][1]) + yOffset, style ); } }, /** * 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. if (options.radarChartMode) { this.drawRadarLabels();} else { 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 || (tick.v < 0 && this.options.radarChartMode)) continue; style.angle = Flotr.toRad(axis.options.labelsAngle); style.halign = 'r'; style.valign = 'm'; ctx.drawText( tick.label, this.plotOffset.left + (this.options.radarChartMode ? this.tHoz(0) : 0) - 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 = ['
']; // 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('
' + tick.label + '
'); } } // 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('
' + tick.label + '
'); } } // 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('
' + tick.label + '
'); } } // 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('
' + tick.label + '
'); 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('
'); 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 = ['
']; // Add title if (options.title){ html.push('
'+options.title+'
'); } // Add subtitle if (options.subtitle){ html.push('
'+options.subtitle+'
'); } html.push('
'); html.push('
'); // Add x axis title if (a.x.options.title && a.x.used){ html.push('
' + a.x.options.title + '
'); } // Add x2 axis title if (a.x2.options.title && a.x2.used){ html.push('
' + a.x2.options.title + '
'); } // Add y axis title if (a.y.options.title && a.y.used){ html.push('
' + a.y.options.title + '
'); } // Add y2 axis title if (a.y2.options.title && a.y2.used){ html.push('
' + a.y2.options.title + '
'); } html.push('
'); 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), tVe