/** Bars **/ Flotr.addType('bars', { options: { 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, // => horizontal bars (x and y inverted) stacked: false, // => stacked bar charts centered: true, // => center the bars to their x axis value topPadding: 0.1, // => top padding in percent grouped: false // => groups bars together which share x value, hit not supported. }, stack : { positive : [], negative : [], _positive : [], // Shadow _negative : [] // Shadow }, draw : function (options) { var context = options.context; this.current += 1; context.save(); context.lineJoin = 'miter'; // @TODO linewidth not interpreted the right way. context.lineWidth = options.lineWidth; context.strokeStyle = options.color; if (options.fill) context.fillStyle = options.fillStyle; this.plot(options); context.restore(); }, plot : function (options) { var data = options.data, context = options.context, shadowSize = options.shadowSize, i, geometry, left, top, width, height; if (data.length < 1) return; this.translate(context, options.horizontal); for (i = 0; i < data.length; i++) { geometry = this.getBarGeometry(data[i][0], data[i][1], options); if (geometry === null) continue; left = geometry.left; top = geometry.top; width = geometry.width; height = geometry.height; if (options.fill) context.fillRect(left, top, width, height); if (shadowSize) { context.save(); context.fillStyle = 'rgba(0,0,0,0.05)'; context.fillRect(left + shadowSize, top + shadowSize, width, height); context.restore(); } if (options.lineWidth) { context.strokeRect(left, top, width, height); } } }, translate : function (context, horizontal) { if (horizontal) { context.rotate(-Math.PI / 2); context.scale(-1, 1); } }, getBarGeometry : function (x, y, options) { var horizontal = options.horizontal, barWidth = options.barWidth, centered = options.centered, stack = options.stacked ? this.stack : false, lineWidth = options.lineWidth, bisection = centered ? barWidth / 2 : 0, xScale = horizontal ? options.yScale : options.xScale, yScale = horizontal ? options.xScale : options.yScale, xValue = horizontal ? y : x, yValue = horizontal ? x : y, stackOffset = 0, stackValue, left, right, top, bottom; if (options.grouped) { this.current / this.groups; xValue = xValue - bisection; barWidth = barWidth / this.groups; bisection = barWidth / 2; xValue = xValue + barWidth * this.current - bisection; } // Stacked bars if (stack) { stackValue = yValue > 0 ? stack.positive : stack.negative; stackOffset = stackValue[xValue] || stackOffset; stackValue[xValue] = stackOffset + yValue; } left = xScale(xValue - bisection); right = xScale(xValue + barWidth - bisection); top = yScale(yValue + stackOffset); bottom = yScale(stackOffset); // TODO for test passing... probably looks better without this if (bottom < 0) bottom = 0; // TODO Skipping... // if (right < xa.min || left > xa.max || top < ya.min || bottom > ya.max) continue; return (x === null || y === null) ? null : { x : xValue, y : yValue, xScale : xScale, yScale : yScale, top : top, left : Math.min(left, right) - lineWidth / 2, width : Math.abs(right - left) - lineWidth, height : bottom - top }; }, hit : function (options) { var data = options.data, args = options.args, mouse = args[0], n = args[1], x = options.xInverse(mouse.relX), y = options.yInverse(mouse.relY), hitGeometry = this.getBarGeometry(x, y, options), width = hitGeometry.width / 2, left = hitGeometry.left, height = hitGeometry.y, geometry, i; for (i = data.length; i--;) { geometry = this.getBarGeometry(data[i][0], data[i][1], options); if ( // Height: ( // Positive Bars: (height > 0 && height < geometry.y) || // Negative Bars: (height < 0 && height > geometry.y) ) && // Width: (Math.abs(left - geometry.left) < width) ) { n.x = data[i][0]; n.y = data[i][1]; n.index = i; n.seriesIndex = options.index; } } }, drawHit : function (options) { // TODO hits for stacked bars; implement using calculateStack option? var context = options.context, args = options.args, geometry = this.getBarGeometry(args.x, args.y, options), left = geometry.left, top = geometry.top, width = geometry.width, height = geometry.height; context.save(); context.strokeStyle = options.color; context.lineWidth = options.lineWidth; this.translate(context, options.horizontal); // Draw highlight context.beginPath(); context.moveTo(left, top + height); context.lineTo(left, top); context.lineTo(left + width, top); context.lineTo(left + width, top + height); if (options.fill) { context.fillStyle = options.fillStyle; context.fill(); } context.stroke(); context.closePath(); context.restore(); }, clearHit: function (options) { var context = options.context, args = options.args, geometry = this.getBarGeometry(args.x, args.y, options), left = geometry.left, width = geometry.width, top = geometry.top, height = geometry.height, lineWidth = 2 * options.lineWidth; context.save(); this.translate(context, options.horizontal); context.clearRect( left - lineWidth, Math.min(top, top + height) - lineWidth, width + 2 * lineWidth, Math.abs(height) + 2 * lineWidth ); context.restore(); }, extendXRange : function (axis, data, options, bars) { this._extendRange(axis, data, options, bars); this.groups = (this.groups + 1) || 1; this.current = 0; }, extendYRange : function (axis, data, options, bars) { this._extendRange(axis, data, options, bars); }, _extendRange: function (axis, data, options, bars) { var max = axis.options.max; if (_.isNumber(max) || _.isString(max)) return; var newmin = axis.min, newmax = axis.max, horizontal = options.horizontal, orientation = axis.orientation, positiveSums = this.positiveSums || {}, negativeSums = this.negativeSums || {}, value, datum, index, j; // Sides of bars if ((orientation == 1 && !horizontal) || (orientation == -1 && horizontal)) { if (options.centered) { newmax = Math.max(axis.datamax + options.barWidth, newmax); newmin = Math.min(axis.datamin - options.barWidth, newmin); } } if (options.stacked && ((orientation == 1 && horizontal) || (orientation == -1 && !horizontal))){ for (j = data.length; j--;) { value = data[j][(orientation == 1 ? 1 : 0)]+''; datum = data[j][(orientation == 1 ? 0 : 1)]; // Positive if (datum > 0) { positiveSums[value] = (positiveSums[value] || 0) + datum; newmax = Math.max(newmax, positiveSums[value]); } // Negative else { negativeSums[value] = (negativeSums[value] || 0) + datum; newmin = Math.min(newmin, negativeSums[value]); } } } // End of bars if ((orientation == 1 && horizontal) || (orientation == -1 && !horizontal)) { if (options.topPadding && (axis.max === axis.datamax || (options.stacked && this.stackMax !== newmax))) { newmax += options.topPadding * (newmax - newmin); } } this.stackMin = newmin; this.stackMax = newmax; this.negativeSums = negativeSums; this.positiveSums = positiveSums; axis.max = newmax; axis.min = newmin; } });