/** * Provides functionality for creating a cartesian chart series. * * @module charts * @submodule series-cartesian */ var Y_Lang = Y.Lang; /** * An abstract class for creating series instances with horizontal and vertical axes. * CartesianSeries provides the core functionality used by the following classes: * <ul> * <li>{{#crossLink "LineSeries"}}{{/crossLink}}</li> * <li>{{#crossLink "MarkerSeries"}}{{/crossLink}}</li> * <li>{{#crossLink "AreaSeries"}}{{/crossLink}}</li> * <li>{{#crossLink "SplineSeries"}}{{/crossLink}}</li> * <li>{{#crossLink "AreaSplineSeries"}}{{/crossLink}}</li> * <li>{{#crossLink "ComboSeries"}}{{/crossLink}}</li> * <li>{{#crossLink "ComboSplineSeries"}}{{/crossLink}}</li> * <li>{{#crossLink "Histogram"}}{{/crossLink}}</li> * </ul> * * @class CartesianSeries * @extends SeriesBase * @constructor * @param {Object} config (optional) Configuration parameters. * @submodule series-base */ Y.CartesianSeries = Y.Base.create("cartesianSeries", Y.SeriesBase, [], { /** * Storage for `xDisplayName` attribute. * * @property _xDisplayName * @type String * @private */ _xDisplayName: null, /** * Storage for `yDisplayName` attribute. * * @property _yDisplayName * @type String * @private */ _yDisplayName: null, /** * Th x-coordinate for the left edge of the series. * * @property _leftOrigin * @type String * @private */ _leftOrigin: null, /** * The y-coordinate for the bottom edge of the series. * * @property _bottomOrigin * @type String * @private */ _bottomOrigin: null, /** * Adds event listeners. * * @method addListeners * @private */ addListeners: function() { var xAxis = this.get("xAxis"), yAxis = this.get("yAxis"); if(xAxis) { this._xDataReadyHandle = xAxis.after("dataReady", Y.bind(this._xDataChangeHandler, this)); this._xDataUpdateHandle = xAxis.after("dataUpdate", Y.bind(this._xDataChangeHandler, this)); } if(yAxis) { this._yDataReadyHandle = yAxis.after("dataReady", Y.bind(this._yDataChangeHandler, this)); this._yDataUpdateHandle = yAxis.after("dataUpdate", Y.bind(this._yDataChangeHandler, this)); } this._xAxisChangeHandle = this.after("xAxisChange", this._xAxisChangeHandler); this._yAxisChangeHandle = this.after("yAxisChange", this._yAxisChangeHandler); this._stylesChangeHandle = this.after("stylesChange", function() { var axesReady = this._updateAxisBase(); if(axesReady) { this.draw(); } }); this._widthChangeHandle = this.after("widthChange", function() { var axesReady = this._updateAxisBase(); if(axesReady) { this.draw(); } }); this._heightChangeHandle = this.after("heightChange", function() { var axesReady = this._updateAxisBase(); if(axesReady) { this.draw(); } }); this._visibleChangeHandle = this.after("visibleChange", this._handleVisibleChange); }, /** * Event handler for the xAxisChange event. * * @method _xAxisChangeHandler * @param {Object} e Event object. * @private */ _xAxisChangeHandler: function() { var xAxis = this.get("xAxis"); xAxis.after("dataReady", Y.bind(this._xDataChangeHandler, this)); xAxis.after("dataUpdate", Y.bind(this._xDataChangeHandler, this)); }, /** * Event handler the yAxisChange event. * * @method _yAxisChangeHandler * @param {Object} e Event object. * @private */ _yAxisChangeHandler: function() { var yAxis = this.get("yAxis"); yAxis.after("dataReady", Y.bind(this._yDataChangeHandler, this)); yAxis.after("dataUpdate", Y.bind(this._yDataChangeHandler, this)); }, /** * Constant used to generate unique id. * * @property GUID * @type String * @private */ GUID: "yuicartesianseries", /** * Event handler for xDataChange event. * * @method _xDataChangeHandler * @param {Object} event Event object. * @private */ _xDataChangeHandler: function() { var axesReady = this._updateAxisBase(); if(axesReady) { this.draw(); } }, /** * Event handler for yDataChange event. * * @method _yDataChangeHandler * @param {Object} event Event object. * @private */ _yDataChangeHandler: function() { var axesReady = this._updateAxisBase(); if(axesReady) { this.draw(); } }, /** * Checks to ensure that both xAxis and yAxis data are available. If so, set the `xData` and `yData` attributes * and return `true`. Otherwise, return `false`. * * @method _updateAxisBase * @return Boolean * @private */ _updateAxisBase: function() { var xAxis = this.get("xAxis"), yAxis = this.get("yAxis"), xKey = this.get("xKey"), yKey = this.get("yKey"), yData, xData, xReady, yReady, ready; if(!xAxis || !yAxis || !xKey || !yKey) { ready = false; } else { xData = xAxis.getDataByKey(xKey); yData = yAxis.getDataByKey(yKey); if(Y_Lang.isArray(xKey)) { xReady = (xData && Y.Object.size(xData) > 0) ? this._checkForDataByKey(xData, xKey) : false; } else { xReady = xData ? true : false; } if(Y_Lang.isArray(yKey)) { yReady = (yData && Y.Object.size(yData) > 0) ? this._checkForDataByKey(yData, yKey) : false; } else { yReady = yData ? true : false; } ready = xReady && yReady; if(ready) { this.set("xData", xData); this.set("yData", yData); } } return ready; }, /** * Checks to see if all keys of a data object exist and contain data. * * @method _checkForDataByKey * @param {Object} obj The object to check * @param {Array} keys The keys to check * @return Boolean * @private */ _checkForDataByKey: function(obj, keys) { var i, len = keys.length, hasData = false; for(i = 0; i < len; i = i + 1) { if(obj[keys[i]]) { hasData = true; break; } } return hasData; }, /** * Draws the series is the xAxis and yAxis data are both available. * * @method validate * @private */ validate: function() { if((this.get("xData") && this.get("yData")) || this._updateAxisBase()) { this.draw(); } else { this.fire("drawingComplete"); } }, /** * Calculates the coordinates for the series. * * @method setAreaData * @protected */ setAreaData: function() { var w = this.get("width"), h = this.get("height"), xAxis = this.get("xAxis"), yAxis = this.get("yAxis"), xData = this._copyData(this.get("xData")), yData = this._copyData(this.get("yData")), direction = this.get("direction"), dataLength = direction === "vertical" ? yData.length : xData.length, xOffset = xAxis.getEdgeOffset(xAxis.getTotalMajorUnits(), w), yOffset = yAxis.getEdgeOffset(yAxis.getTotalMajorUnits(), h), padding = this.get("styles").padding, leftPadding = padding.left, topPadding = padding.top, dataWidth = w - (leftPadding + padding.right + xOffset * 2), dataHeight = h - (topPadding + padding.bottom + yOffset * 2), xMax = xAxis.get("maximum"), xMin = xAxis.get("minimum"), yMax = yAxis.get("maximum"), yMin = yAxis.get("minimum"), graphic = this.get("graphic"), yAxisType = yAxis.get("type"), reverseYCoords = (yAxisType === "numeric" || yAxisType === "stacked"), xcoords, ycoords, xOriginValue = xAxis.getOrigin(), yOriginValue = yAxis.getOrigin(); graphic.set("width", w); graphic.set("height", h); xOffset = xOffset + leftPadding; yOffset = reverseYCoords ? yOffset + dataHeight + topPadding + padding.bottom : topPadding + yOffset; this._leftOrigin = Math.round(xAxis._getCoordFromValue(xMin, xMax, dataWidth, xOriginValue, xOffset, false)); this._bottomOrigin = Math.round(yAxis._getCoordFromValue(yMin, yMax, dataHeight, yOriginValue, yOffset, reverseYCoords)); xcoords = this._getCoords(xMin, xMax, dataWidth, xData, xAxis, xOffset, false); ycoords = this._getCoords(yMin, yMax, dataHeight, yData, yAxis, yOffset, reverseYCoords); this.set("xcoords", xcoords); this.set("ycoords", ycoords); this._dataLength = dataLength; this._setXMarkerPlane(xcoords, dataLength); this._setYMarkerPlane(ycoords, dataLength); }, /** * Returns either an array coordinates or an object key valued arrays of coordinates depending on the input. * If the input data is an array, an array is returned. If the input data is an object, an object will be returned. * * @method _getCoords * @param {Number} min The minimum value of the range of data. * @param {Number} max The maximum value of the range of data. * @param {Number} length The length, in pixels, of across which the coordinates will be calculated. * @param {AxisBase} axis The axis in which the data is bound. * @param {Number} offset The value in which to offet the first coordinate. * @param {Boolean} reverse Indicates whether to calculate the coordinates in reverse order. * @return Array|Object * @private */ _getCoords: function(min, max, length, data, axis, offset, reverse) { var coords, key; if(Y_Lang.isArray(data)) { coords = axis._getCoordsFromValues(min, max, length, data, offset, reverse); } else { coords = {}; for(key in data) { if(data.hasOwnProperty(key)) { coords[key] = this._getCoords.apply(this, [min, max, length, data[key], axis, offset, reverse]); } } } return coords; }, /** * Used to cache xData and yData in the setAreaData method. Returns a copy of an * array if an array is received as the param and returns an object literal of * array copies if an object literal is received as the param. * * @method _copyData * @param {Array|Object} val The object or array to be copied. * @return Array|Object * @private */ _copyData: function(val) { var copy, key; if(Y_Lang.isArray(val)) { copy = val.concat(); } else { copy = {}; for(key in val) { if(val.hasOwnProperty(key)) { copy[key] = val[key].concat(); } } } return copy; }, /** * Sets the marker plane for the series when the coords argument is an array. * If the coords argument is an object literal no marker plane is set. * * @method _setXMarkerPlane * @param {Array|Object} coords An array of x coordinates or an object literal * containing key value pairs mapped to an array of coordinates. * @param {Number} dataLength The length of data for the series. * @private */ _setXMarkerPlane: function(coords, dataLength) { var i = 0, xMarkerPlane = [], xMarkerPlaneOffset = this.get("xMarkerPlaneOffset"), nextX; if(Y_Lang.isArray(coords)) { for(i = 0; i < dataLength; i = i + 1) { nextX = coords[i]; xMarkerPlane.push({start:nextX - xMarkerPlaneOffset, end: nextX + xMarkerPlaneOffset}); } this.set("xMarkerPlane", xMarkerPlane); } }, /** * Sets the marker plane for the series when the coords argument is an array. * If the coords argument is an object literal no marker plane is set. * * @method _setYMarkerPlane * @param {Array|Object} coords An array of y coordinates or an object literal * containing key value pairs mapped to an array of coordinates. * @param {Number} dataLength The length of data for the series. * @private */ _setYMarkerPlane: function(coords, dataLength) { var i = 0, yMarkerPlane = [], yMarkerPlaneOffset = this.get("yMarkerPlaneOffset"), nextY; if(Y_Lang.isArray(coords)) { for(i = 0; i < dataLength; i = i + 1) { nextY = coords[i]; yMarkerPlane.push({start:nextY - yMarkerPlaneOffset, end: nextY + yMarkerPlaneOffset}); } this.set("yMarkerPlane", yMarkerPlane); } }, /** * Finds the first valid index of an array coordinates. * * @method _getFirstValidIndex * @param {Array} coords An array of x or y coordinates. * @return Number * @private */ _getFirstValidIndex: function(coords) { var coord, i = -1, limit = coords.length; while(!Y_Lang.isNumber(coord) && i < limit) { i += 1; coord = coords[i]; } return i; }, /** * Finds the last valid index of an array coordinates. * * @method _getLastValidIndex * @param {Array} coords An array of x or y coordinates. * @return Number * @private */ _getLastValidIndex: function(coords) { var coord, i = coords.length, limit = -1; while(!Y_Lang.isNumber(coord) && i > limit) { i -= 1; coord = coords[i]; } return i; }, /** * Draws the series. * * @method draw * @protected */ draw: function() { var w = this.get("width"), h = this.get("height"), xcoords, ycoords; if(this.get("rendered")) { if((isFinite(w) && isFinite(h) && w > 0 && h > 0) && ((this.get("xData") && this.get("yData")) || this._updateAxisBase())) { if(this._drawing) { this._callLater = true; return; } this._drawing = true; this._callLater = false; this.setAreaData(); xcoords = this.get("xcoords"); ycoords = this.get("ycoords"); if(xcoords && ycoords && xcoords.length > 0) { this.drawSeries(); } this._drawing = false; if(this._callLater) { this.draw(); } else { this._toggleVisible(this.get("visible")); this.fire("drawingComplete"); } } } }, /** * Default value for plane offsets when the parent chart's `interactiveType` is `planar`. * * @property _defaultPlaneOffset * @type Number * @private */ _defaultPlaneOffset: 4, /** * Destructor implementation for the CartesianSeries class. * Calls destroy on all Graphic instances. * * @method destructor * @protected */ destructor: function() { if(this.get("rendered")) { if(this._xDataReadyHandle) { this._xDataReadyHandle.detach(); } if(this._xDataUpdateHandle) { this._xDataUpdateHandle.detach(); } if(this._yDataReadyHandle) { this._yDataReadyHandle.detach(); } if(this._yDataUpdateHandle) { this._yDataUpdateHandle.detach(); } if(this._xAxisChangeHandle) { this._xAxisChangeHandle.detach(); } if(this._yAxisChangeHandle) { this._yAxisChangeHandle.detach(); } } } /** * Event handle for the x-axis' dataReady event. * * @property _xDataReadyHandle * @type {EventHandle} * @private */ /** * Event handle for the x-axis dataUpdate event. * * @property _xDataUpdateHandle * @type {EventHandle} * @private */ /** * Event handle for the y-axis dataReady event. * * @property _yDataReadyHandle * @type {EventHandle} * @private */ /** * Event handle for the y-axis dataUpdate event. * @property _yDataUpdateHandle * @type {EventHandle} * @private */ /** * Event handle for the xAxisChange event. * @property _xAxisChangeHandle * @type {EventHandle} * @private */ /** * Event handle for the yAxisChange event. * @property _yAxisChangeHandle * @type {EventHandle} * @private */ /** * Event handle for the stylesChange event. * @property _stylesChangeHandle * @type {EventHandle} * @private */ /** * Event handle for the widthChange event. * @property _widthChangeHandle * @type {EventHandle} * @private */ /** * Event handle for the heightChange event. * @property _heightChangeHandle * @type {EventHandle} * @private */ /** * Event handle for the visibleChange event. * @property _visibleChangeHandle * @type {EventHandle} * @private */ }, { ATTRS: { /** * An array of all series of the same type used within a chart application. * * @attribute seriesTypeCollection * @type Array */ seriesTypeCollection: {}, /** * Name used for for displaying data related to the x-coordinate. * * @attribute xDisplayName * @type String */ xDisplayName: { getter: function() { return this._xDisplayName || this.get("xKey"); }, setter: function(val) { this._xDisplayName = val.toString(); return val; } }, /** * Name used for for displaying data related to the y-coordinate. * * @attribute yDisplayName * @type String */ yDisplayName: { getter: function() { return this._yDisplayName || this.get("yKey"); }, setter: function(val) { this._yDisplayName = val.toString(); return val; } }, /** * Name used for for displaying category data * * @attribute categoryDisplayName * @type String * @readOnly */ categoryDisplayName: { lazyAdd: false, getter: function() { return this.get("direction") === "vertical" ? this.get("yDisplayName") : this.get("xDisplayName"); }, setter: function(val) { if(this.get("direction") === "vertical") { this._yDisplayName = val; } else { this._xDisplayName = val; } return val; } }, /** * Name used for for displaying value data * * @attribute valueDisplayName * @type String * @readOnly */ valueDisplayName: { lazyAdd: false, getter: function() { return this.get("direction") === "vertical" ? this.get("xDisplayName") : this.get("yDisplayName"); }, setter: function(val) { if(this.get("direction") === "vertical") { this._xDisplayName = val; } else { this._yDisplayName = val; } return val; } }, /** * Read-only attribute indicating the type of series. * * @attribute type * @type String * @default cartesian */ type: { value: "cartesian" }, /** * Order of this instance of this `type`. * * @attribute order * @type Number */ order: {}, /** * Order of the instance * * @attribute graphOrder * @type Number */ graphOrder: {}, /** * x coordinates for the series. * * @attribute xcoords * @type Array */ xcoords: {}, /** * y coordinates for the series * * @attribute ycoords * @type Array */ ycoords: {}, /** * Reference to the `Axis` instance used for assigning * x-values to the graph. * * @attribute xAxis * @type Axis */ xAxis: {}, /** * Reference to the `Axis` instance used for assigning * y-values to the graph. * * @attribute yAxis * @type Axis */ yAxis: {}, /** * Indicates which array to from the hash of value arrays in * the x-axis `Axis` instance. * * @attribute xKey * @type String */ xKey: { setter: function(val) { if(Y_Lang.isArray(val)) { return val; } else { return val.toString(); } } }, /** * Indicates which array to from the hash of value arrays in * the y-axis `Axis` instance. * * @attribute yKey * @type String */ yKey: { setter: function(val) { if(Y_Lang.isArray(val)) { return val; } else { return val.toString(); } } }, /** * Array of x values for the series. * * @attribute xData * @type Array */ xData: {}, /** * Array of y values for the series. * * @attribute yData * @type Array */ yData: {}, /** * Collection of area maps along the xAxis. Used to determine mouseover for multiple * series. * * @attribute xMarkerPlane * @type Array */ xMarkerPlane: {}, /** * Collection of area maps along the yAxis. Used to determine mouseover for multiple * series. * * @attribute yMarkerPlane * @type Array */ yMarkerPlane: {}, /** * Distance from a data coordinate to the left/right for setting a hotspot. * * @attribute xMarkerPlaneOffset * @type Number */ xMarkerPlaneOffset: { getter: function() { var marker = this.get("styles").marker; if(marker && marker.width && isFinite(marker.width)) { return marker.width * 0.5; } return this._defaultPlaneOffset; } }, /** * Distance from a data coordinate to the top/bottom for setting a hotspot. * * @attribute yMarkerPlaneOffset * @type Number */ yMarkerPlaneOffset: { getter: function() { var marker = this.get("styles").marker; if(marker && marker.height && isFinite(marker.height)) { return marker.height * 0.5; } return this._defaultPlaneOffset; } }, /** * Direction of the series * * @attribute direction * @type String */ direction: { value: "horizontal" } } });