/** * Provides functionality for drawing fills in a series. * * @module charts * @submodule series-fill-util */ var Y_Lang = Y.Lang; /** * Utility class used for drawing area fills. * * @class Fills * @constructor * @submodule series-fill-util */ function Fills() {} Fills.ATTRS = { area: { getter: function() { return this._defaults || this._getAreaDefaults(); }, setter: function(val) { var defaults = this._defaults || this._getAreaDefaults(); this._defaults = Y.merge(defaults, val); } } }; Fills.prototype = { /** * Returns a path shape used for drawing fills. * * @method _getPath * @return Path * @private */ _getPath: function() { var path = this._path; if(!path) { path = this.get("graphic").addShape({type:"path"}); this._path = path; } return path; }, /** * Toggles visibility * * @method _toggleVisible * @param {Boolean} visible indicates visibilitye * @private */ _toggleVisible: function(visible) { if(this._path) { this._path.set("visible", visible); } }, /** * Draws fill * * @method drawFill * @param {Array} xcoords The x-coordinates for the series. * @param {Array} ycoords The y-coordinates for the series. * @protected */ drawFill: function(xcoords, ycoords) { if(xcoords.length < 1) { return; } var isNumber = Y_Lang.isNumber, len = xcoords.length, firstX = xcoords[0], firstY = ycoords[0], lastValidX = firstX, lastValidY = firstY, nextX, nextY, pointValid, noPointsRendered = true, i = 0, styles = this.get("styles").area, path = this._getPath(), color = styles.color || this._getDefaultColor(this.get("graphOrder"), "slice"); path.clear(); path.set("fill", { color: color, opacity: styles.alpha }); path.set("stroke", {weight: 0}); for(; i < len; i = ++i) { nextX = xcoords[i]; nextY = ycoords[i]; pointValid = isNumber(nextX) && isNumber(nextY); if(!pointValid) { continue; } if(noPointsRendered) { this._firstValidX = nextX; this._firstValidY = nextY; noPointsRendered = false; path.moveTo(nextX, nextY); } else { path.lineTo(nextX, nextY); } lastValidX = nextX; lastValidY = nextY; } this._lastValidX = lastValidX; this._lastValidY = lastValidY; path.end(); }, /** * Draws a fill for a spline * * @method drawAreaSpline * @protected */ drawAreaSpline: function() { if(this.get("xcoords").length < 1) { return; } var xcoords = this.get("xcoords"), ycoords = this.get("ycoords"), curvecoords = this.getCurveControlPoints(xcoords, ycoords), len = curvecoords.length, cx1, cx2, cy1, cy2, x, y, i = 0, firstX = xcoords[0], firstY = ycoords[0], styles = this.get("styles").area, path = this._getPath(), color = styles.color || this._getDefaultColor(this.get("graphOrder"), "slice"); path.set("fill", { color: color, opacity: styles.alpha }); path.set("stroke", {weight: 0}); path.moveTo(firstX, firstY); for(; i < len; i = ++i) { x = curvecoords[i].endx; y = curvecoords[i].endy; cx1 = curvecoords[i].ctrlx1; cx2 = curvecoords[i].ctrlx2; cy1 = curvecoords[i].ctrly1; cy2 = curvecoords[i].ctrly2; path.curveTo(cx1, cy1, cx2, cy2, x, y); } if(this.get("direction") === "vertical") { path.lineTo(this._leftOrigin, y); path.lineTo(this._leftOrigin, firstY); } else { path.lineTo(x, this._bottomOrigin); path.lineTo(firstX, this._bottomOrigin); } path.lineTo(firstX, firstY); path.end(); }, /** * Draws a a stacked area spline * * @method drawStackedAreaSpline * @protected */ drawStackedAreaSpline: function() { if(this.get("xcoords").length < 1) { return; } var xcoords = this.get("xcoords"), ycoords = this.get("ycoords"), curvecoords, order = this.get("order"), seriesCollection = this.get("seriesTypeCollection"), prevXCoords, prevYCoords, len, cx1, cx2, cy1, cy2, x, y, i = 0, firstX, firstY, styles = this.get("styles").area, path = this._getPath(), color = styles.color || this._getDefaultColor(this.get("graphOrder"), "slice"); firstX = xcoords[0]; firstY = ycoords[0]; curvecoords = this.getCurveControlPoints(xcoords, ycoords); len = curvecoords.length; path.set("fill", { color: color, opacity: styles.alpha }); path.set("stroke", {weight: 0}); path.moveTo(firstX, firstY); for(; i < len; i = ++i) { x = curvecoords[i].endx; y = curvecoords[i].endy; cx1 = curvecoords[i].ctrlx1; cx2 = curvecoords[i].ctrlx2; cy1 = curvecoords[i].ctrly1; cy2 = curvecoords[i].ctrly2; path.curveTo(cx1, cy1, cx2, cy2, x, y); } if(order > 0) { prevXCoords = seriesCollection[order - 1].get("xcoords").concat().reverse(); prevYCoords = seriesCollection[order - 1].get("ycoords").concat().reverse(); curvecoords = this.getCurveControlPoints(prevXCoords, prevYCoords); i = 0; len = curvecoords.length; path.lineTo(prevXCoords[0], prevYCoords[0]); for(; i < len; i = ++i) { x = curvecoords[i].endx; y = curvecoords[i].endy; cx1 = curvecoords[i].ctrlx1; cx2 = curvecoords[i].ctrlx2; cy1 = curvecoords[i].ctrly1; cy2 = curvecoords[i].ctrly2; path.curveTo(cx1, cy1, cx2, cy2, x, y); } } else { if(this.get("direction") === "vertical") { path.lineTo(this._leftOrigin, ycoords[ycoords.length-1]); path.lineTo(this._leftOrigin, firstY); } else { path.lineTo(xcoords[xcoords.length-1], this._bottomOrigin); path.lineTo(firstX, this._bottomOrigin); } } path.lineTo(firstX, firstY); path.end(); }, /** * Storage for default area styles. * * @property _defaults * @type Object * @private */ _defaults: null, /** * Concatenates coordinate array with correct coordinates for closing an area fill. * * @method _getClosingPoints * @return Array * @protected */ _getClosingPoints: function() { var xcoords = this.get("xcoords").concat(), ycoords = this.get("ycoords").concat(), firstValidIndex, lastValidIndex; if(this.get("direction") === "vertical") { lastValidIndex = this._getLastValidIndex(xcoords); firstValidIndex = this._getFirstValidIndex(xcoords); ycoords.push(ycoords[lastValidIndex]); ycoords.push(ycoords[firstValidIndex]); xcoords.push(this._leftOrigin); xcoords.push(this._leftOrigin); } else { lastValidIndex = this._getLastValidIndex(ycoords); firstValidIndex = this._getFirstValidIndex(ycoords); xcoords.push(xcoords[lastValidIndex]); xcoords.push(xcoords[firstValidIndex]); ycoords.push(this._bottomOrigin); ycoords.push(this._bottomOrigin); } xcoords.push(xcoords[0]); ycoords.push(ycoords[0]); return [xcoords, ycoords]; }, /** * Returns the order of the series closest to the current series that has a valid value for the current index. * * @method _getHighestValidOrder * @param {Array} seriesCollection Array of series of a given type. * @param {Number} index Index of the series item. * @param {Number} order Index of the the series in the seriesCollection * @param {String} direction Indicates the direction of the series * @return Number * @private */ _getHighestValidOrder: function(seriesCollection, index, order, direction) { var coords = direction === "vertical" ? "stackedXCoords" : "stackedYCoords", coord; while(isNaN(coord) && order > -1) { order = order - 1; if(order > -1) { coord = seriesCollection[order].get(coords)[index]; } } return order; }, /** * Returns an array containing the x and y coordinates for a given series and index. * * @method _getCoordsByOrderAndIndex * @param {Array} seriesCollection Array of series of a given type. * @param {Number} index Index of the series item. * @param {Number} order Index of the the series in the seriesCollection * @param {String} direction Indicates the direction of the series * @return Array * @private */ _getCoordsByOrderAndIndex: function(seriesCollection, index, order, direction) { var xcoord, ycoord; if(direction === "vertical") { xcoord = order < 0 ? this._leftOrigin : seriesCollection[order].get("stackedXCoords")[index]; ycoord = this.get("stackedYCoords")[index]; } else { xcoord = this.get("stackedXCoords")[index]; ycoord = order < 0 ? this._bottomOrigin : seriesCollection[order].get("stackedYCoords")[index]; } return [xcoord, ycoord]; }, /** * Concatenates coordinate array with the correct coordinates for closing an area stack. * * @method _getStackedClosingPoints * @return Array * @protected */ _getStackedClosingPoints: function() { var order = this.get("order"), direction = this.get("direction"), seriesCollection = this.get("seriesTypeCollection"), firstValidIndex, lastValidIndex, xcoords = this.get("stackedXCoords"), ycoords = this.get("stackedYCoords"), limit, previousSeries, previousSeriesFirstValidIndex, previousSeriesLastValidIndex, previousXCoords, previousYCoords, coords, closingXCoords, closingYCoords, currentIndex, highestValidOrder, oldOrder; if(order < 1) { return this._getClosingPoints(); } previousSeries = seriesCollection[order - 1]; previousXCoords = previousSeries.get("stackedXCoords").concat(); previousYCoords = previousSeries.get("stackedYCoords").concat(); if(direction === "vertical") { firstValidIndex = this._getFirstValidIndex(xcoords); lastValidIndex = this._getLastValidIndex(xcoords); previousSeriesFirstValidIndex = previousSeries._getFirstValidIndex(previousXCoords); previousSeriesLastValidIndex = previousSeries._getLastValidIndex(previousXCoords); } else { firstValidIndex = this._getFirstValidIndex(ycoords); lastValidIndex = this._getLastValidIndex(ycoords); previousSeriesFirstValidIndex = previousSeries._getFirstValidIndex(previousYCoords); previousSeriesLastValidIndex = previousSeries._getLastValidIndex(previousYCoords); } if(previousSeriesLastValidIndex >= firstValidIndex && previousSeriesFirstValidIndex <= lastValidIndex) { previousSeriesFirstValidIndex = Math.max(firstValidIndex, previousSeriesFirstValidIndex); previousSeriesLastValidIndex = Math.min(lastValidIndex, previousSeriesLastValidIndex); previousXCoords = previousXCoords.slice(previousSeriesFirstValidIndex, previousSeriesLastValidIndex + 1); previousYCoords = previousYCoords.slice(previousSeriesFirstValidIndex, previousSeriesLastValidIndex + 1); limit = previousSeriesFirstValidIndex; } else { limit = lastValidIndex; } closingXCoords = [xcoords[firstValidIndex]]; closingYCoords = [ycoords[firstValidIndex]]; currentIndex = firstValidIndex; while((isNaN(highestValidOrder) || highestValidOrder < order - 1) && currentIndex <= limit) { oldOrder = highestValidOrder; highestValidOrder = this._getHighestValidOrder(seriesCollection, currentIndex, order, direction); if(!isNaN(oldOrder) && highestValidOrder > oldOrder) { coords = this._getCoordsByOrderAndIndex(seriesCollection, currentIndex, oldOrder, direction); closingXCoords.push(coords[0]); closingYCoords.push(coords[1]); } coords = this._getCoordsByOrderAndIndex(seriesCollection, currentIndex, highestValidOrder, direction); closingXCoords.push(coords[0]); closingYCoords.push(coords[1]); currentIndex = currentIndex + 1; } if(previousXCoords && previousXCoords.length > 0 && previousSeriesLastValidIndex > firstValidIndex && previousSeriesFirstValidIndex < lastValidIndex) { closingXCoords = closingXCoords.concat(previousXCoords); closingYCoords = closingYCoords.concat(previousYCoords); highestValidOrder = order -1; } currentIndex = Math.max(firstValidIndex, previousSeriesLastValidIndex); order = order - 1; highestValidOrder = NaN; while(currentIndex <= lastValidIndex) { oldOrder = highestValidOrder; highestValidOrder = this._getHighestValidOrder(seriesCollection, currentIndex, order, direction); if(!isNaN(oldOrder)) { if(highestValidOrder > oldOrder) { coords = this._getCoordsByOrderAndIndex(seriesCollection, currentIndex, oldOrder, direction); closingXCoords.push(coords[0]); closingYCoords.push(coords[1]); } else if(highestValidOrder < oldOrder) { coords = this._getCoordsByOrderAndIndex(seriesCollection, currentIndex - 1, highestValidOrder, direction); closingXCoords.push(coords[0]); closingYCoords.push(coords[1]); } } coords = this._getCoordsByOrderAndIndex(seriesCollection, currentIndex, highestValidOrder, direction); closingXCoords.push(coords[0]); closingYCoords.push(coords[1]); currentIndex = currentIndex + 1; } closingXCoords.reverse(); closingYCoords.reverse(); return [xcoords.concat(closingXCoords), ycoords.concat(closingYCoords)]; }, /** * Returns default values for area styles. * * @method _getAreaDefaults * @return Object * @private */ _getAreaDefaults: function() { return { }; } }; Y.augment(Fills, Y.Attribute); Y.Fills = Fills;