/** * Provides functionality for creating a pie series. * * @module charts * @submodule series-pie */ /** * PieSeries visualizes data as a circular chart divided into wedges which represent data as a * percentage of a whole. * * @class PieSeries * @constructor * @extends SeriesBase * @uses Plots * @param {Object} config (optional) Configuration parameters. * @submodule series-pie */ var CONFIG = Y.config, DOCUMENT = CONFIG.doc, _getClassName = Y.ClassNameManager.getClassName, SERIES_MARKER = _getClassName("seriesmarker"); Y.PieSeries = Y.Base.create("pieSeries", Y.SeriesBase, [Y.Plots], { /** * Image map used for interactivity when rendered with canvas. * * @property _map * @type HTMLElement * @private */ _map: null, /** * Image used for image map when rendered with canvas. * * @property _image * @type HTMLElement * @private */ _image: null, /** * Creates or updates the image map when rendered with canvas. * * @method _setMap * @private */ _setMap: function() { var id = "pieHotSpotMapi_" + Math.round(100000 * Math.random()), graph = this.get("graph"), graphic, cb, areaNode; if(graph) { cb = graph.get("contentBox"); } else { graphic = this.get("graphic"); cb = graphic.get("node"); } if(this._image) { cb.removeChild(this._image); while(this._areaNodes && this._areaNodes.length > 0) { areaNode = this._areaNodes.shift(); this._map.removeChild(areaNode); } cb.removeChild(this._map); } this._image = DOCUMENT.createElement("img"); this._image.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAABCAYAAAD9yd/wAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSB" + "JbWFnZVJlYWR5ccllPAAAABJJREFUeNpiZGBgSGPAAgACDAAIkABoFyloZQAAAABJRU5ErkJggg=="; cb.appendChild(this._image); this._image.style.position = "absolute"; this._image.style.left = "0px"; this._image.style.top = "0px"; this._image.setAttribute("usemap", "#" + id); this._image.style.zIndex = 3; this._image.style.opacity = 0; this._image.setAttribute("alt", "imagemap"); this._map = DOCUMENT.createElement("map"); cb.appendChild(this._map); this._map.setAttribute("name", id); this._map.setAttribute("id", id); this._areaNodes = []; }, /** * Storage for `categoryDisplayName` attribute. * * @property _categoryDisplayName * @private */ _categoryDisplayName: null, /** * Storage for `valueDisplayName` attribute. * * @property _valueDisplayName * @private */ _valueDisplayName: null, /** * Adds event listeners. * * @method addListeners * @private */ addListeners: function() { var categoryAxis = this.get("categoryAxis"), valueAxis = this.get("valueAxis"); if(categoryAxis) { categoryAxis.after("dataReady", Y.bind(this._categoryDataChangeHandler, this)); categoryAxis.after("dataUpdate", Y.bind(this._categoryDataChangeHandler, this)); } if(valueAxis) { valueAxis.after("dataReady", Y.bind(this._valueDataChangeHandler, this)); valueAxis.after("dataUpdate", Y.bind(this._valueDataChangeHandler, this)); } this.after("categoryAxisChange", this.categoryAxisChangeHandler); this.after("valueAxisChange", this.valueAxisChangeHandler); this._stylesChangeHandle = this.after("stylesChange", this._updateHandler); this._visibleChangeHandle = this.after("visibleChange", this._handleVisibleChange); }, /** * Draws the series. * * @method validate * @private */ validate: function() { this.draw(); this._renderered = true; }, /** * Event handler for the categoryAxisChange event. * * @method _categoryAxisChangeHandler * @param {Object} e Event object. * @private */ _categoryAxisChangeHandler: function() { var categoryAxis = this.get("categoryAxis"); categoryAxis.after("dataReady", Y.bind(this._categoryDataChangeHandler, this)); categoryAxis.after("dataUpdate", Y.bind(this._categoryDataChangeHandler, this)); }, /** * Event handler for the valueAxisChange event. * * @method _valueAxisChangeHandler * @param {Object} e Event object. * @private */ _valueAxisChangeHandler: function() { var valueAxis = this.get("valueAxis"); valueAxis.after("dataReady", Y.bind(this._valueDataChangeHandler, this)); valueAxis.after("dataUpdate", Y.bind(this._valueDataChangeHandler, this)); }, /** * Constant used to generate unique id. * * @property GUID * @type String * @private */ GUID: "pieseries", /** * Event handler for categoryDataChange event. * * @method _categoryDataChangeHandler * @param {Object} event Event object. * @private */ _categoryDataChangeHandler: function() { if(this._rendered && this.get("categoryKey") && this.get("valueKey")) { this.draw(); } }, /** * Event handler for valueDataChange event. * * @method _valueDataChangeHandler * @param {Object} event Event object. * @private */ _valueDataChangeHandler: function() { if(this._rendered && this.get("categoryKey") && this.get("valueKey")) { this.draw(); } }, /** * Returns the sum of all values for the series. * * @method getTotalValues * @return Number */ getTotalValues: function() { var total = this.get("valueAxis").getTotalByKey(this.get("valueKey")); return total; }, /** * Draws the series. Overrides the base implementation. * * @method draw * @protected */ draw: function() { var w = this.get("width"), h = this.get("height"); if(isFinite(w) && isFinite(h) && w > 0 && h > 0) { this._rendered = true; if(this._drawing) { this._callLater = true; return; } this._drawing = true; this._callLater = false; this.drawSeries(); this._drawing = false; if(this._callLater) { this.draw(); } else { this.fire("drawingComplete"); } } }, /** * Draws the markers * * @method drawPlots * @protected */ drawPlots: function() { var values = this.get("valueAxis").getDataByKey(this.get("valueKey")).concat(), totalValue = 0, itemCount = values.length, styles = this.get("styles").marker, fillColors = styles.fill.colors, fillAlphas = styles.fill.alphas || ["1"], borderColors = styles.border.colors, borderWeights = [styles.border.weight], borderAlphas = [styles.border.alpha], tbw = borderWeights.concat(), tbc = borderColors.concat(), tba = borderAlphas.concat(), tfc, tfa, padding = styles.padding, graphic = this.get("graphic"), minDimension = Math.min(graphic.get("width"), graphic.get("height")), w = minDimension - (padding.left + padding.right), h = minDimension - (padding.top + padding.bottom), startAngle = -90, halfWidth = w / 2, halfHeight = h / 2, radius = Math.min(halfWidth, halfHeight), i = 0, value, angle = 0, lc, la, lw, wedgeStyle, marker, graphOrder = this.get("graphOrder") || 0, isCanvas = Y.Graphic.NAME === "canvasGraphic"; for(; i < itemCount; ++i) { value = parseFloat(values[i]); values.push(value); if(!isNaN(value)) { totalValue += value; } } tfc = fillColors ? fillColors.concat() : null; tfa = fillAlphas ? fillAlphas.concat() : null; this._createMarkerCache(); if(isCanvas) { this._setMap(); this._image.width = w; this._image.height = h; } for(i = 0; i < itemCount; i++) { value = values[i]; if(totalValue === 0) { angle = 360 / values.length; } else { angle = 360 * (value / totalValue); } if(tfc && tfc.length < 1) { tfc = fillColors.concat(); } if(tfa && tfa.length < 1) { tfa = fillAlphas.concat(); } if(tbw && tbw.length < 1) { tbw = borderWeights.concat(); } if(tbw && tbc.length < 1) { tbc = borderColors.concat(); } if(tba && tba.length < 1) { tba = borderAlphas.concat(); } lw = tbw ? tbw.shift() : null; lc = tbc ? tbc.shift() : null; la = tba ? tba.shift() : null; startAngle += angle; wedgeStyle = { border: { color:lc, weight:lw, alpha:la }, fill: { color:tfc ? tfc.shift() : this._getDefaultColor(i, "slice"), alpha:tfa ? tfa.shift() : null }, type: "pieslice", arc: angle, radius: radius, startAngle: startAngle, cx: halfWidth, cy: halfHeight, width: w, height: h }; marker = this.getMarker(wedgeStyle, graphOrder, i); if(isCanvas) { this._addHotspot(wedgeStyle, graphOrder, i); } } this._clearMarkerCache(); }, /** * @protected * * Method used by `styles` setter. Overrides base implementation. * * @method _setStyles * @param {Object} newStyles Hash of properties to update. * @return Object */ _setStyles: function(val) { if(!val.marker) { val = {marker:val}; } val = this._parseMarkerStyles(val); return Y.PieSeries.superclass._mergeStyles.apply(this, [val, this._getDefaultStyles()]); }, /** * Adds an interactive map when rendering in canvas. * * @method _addHotspot * @param {Object} cfg Object containing data used to draw the hotspot * @param {Number} seriesIndex Index of series in the `seriesCollection`. * @param {Number} index Index of the marker using the hotspot. * @private */ _addHotspot: function(cfg, seriesIndex, index) { var areaNode = DOCUMENT.createElement("area"), i = 1, x = cfg.cx, y = cfg.cy, arc = cfg.arc, startAngle = cfg.startAngle - arc, endAngle = cfg.startAngle, radius = cfg.radius, ax = x + Math.cos(startAngle / 180 * Math.PI) * radius, ay = y + Math.sin(startAngle / 180 * Math.PI) * radius, bx = x + Math.cos(endAngle / 180 * Math.PI) * radius, by = y + Math.sin(endAngle / 180 * Math.PI) * radius, numPoints = Math.floor(arc/10) - 1, divAngle = (arc/(Math.floor(arc/10)) / 180) * Math.PI, angleCoord = Math.atan((ay - y)/(ax - x)), pts = x + ", " + y + ", " + ax + ", " + ay, cosAng, sinAng, multDivAng; for(i = 1; i <= numPoints; ++i) { multDivAng = divAngle * i; cosAng = Math.cos(angleCoord + multDivAng); sinAng = Math.sin(angleCoord + multDivAng); if(startAngle <= 90) { pts += ", " + (x + (radius * Math.cos(angleCoord + (divAngle * i)))); pts += ", " + (y + (radius * Math.sin(angleCoord + (divAngle * i)))); } else { pts += ", " + (x - (radius * Math.cos(angleCoord + (divAngle * i)))); pts += ", " + (y - (radius * Math.sin(angleCoord + (divAngle * i)))); } } pts += ", " + bx + ", " + by; pts += ", " + x + ", " + y; this._map.appendChild(areaNode); areaNode.setAttribute("class", SERIES_MARKER); areaNode.setAttribute("id", "hotSpot_" + seriesIndex + "_" + index); areaNode.setAttribute("shape", "polygon"); areaNode.setAttribute("coords", pts); this._areaNodes.push(areaNode); }, /** * Resizes and positions markers based on a mouse interaction. * * @method updateMarkerState * @param {String} type state of the marker * @param {Number} i index of the marker * @protected */ updateMarkerState: function(type, i) { if(this._markers[i]) { var state = this._getState(type), markerStyles, indexStyles, marker = this._markers[i], styles = this.get("styles").marker; markerStyles = state === "off" || !styles[state] ? styles : styles[state]; indexStyles = this._mergeStyles(markerStyles, {}); indexStyles.fill.color = indexStyles.fill.colors[i % indexStyles.fill.colors.length]; indexStyles.fill.alpha = indexStyles.fill.alphas[i % indexStyles.fill.alphas.length]; marker.set(indexStyles); } }, /** * Creates a shape to be used as a marker. * * @method _createMarker * @param {Object} styles Hash of style properties. * @return Shape * @private */ _createMarker: function(styles) { var graphic = this.get("graphic"), marker, cfg = this._copyObject(styles); marker = graphic.addShape(cfg); marker.addClass(SERIES_MARKER); return marker; }, /** * Creates a cache of markers for reuse. * * @method _createMarkerCache * @private */ _clearMarkerCache: function() { var len = this._markerCache.length, i = 0, marker; for(; i < len; ++i) { marker = this._markerCache[i]; if(marker) { marker.destroy(); } } this._markerCache = []; }, /** * Gets the default style values for the markers. * * @method _getPlotDefaults * @return Object * @private */ _getPlotDefaults: function() { var defs = { padding:{ top: 0, left: 0, right: 0, bottom: 0 }, fill:{ alphas:["1"] }, border: { weight: 0, alpha: 1 } }; defs.fill.colors = this._defaultSliceColors; defs.border.colors = this._defaultBorderColors; return defs; } }, { ATTRS: { /** * Read-only attribute indicating the type of series. * * @attribute type * @type String * @default pie */ type: { value: "pie" }, /** * Order of this instance of this `type`. * * @attribute order * @type Number */ order: {}, /** * Reference to the `Graph` in which the series is drawn into. * * @attribute graph * @type Graph */ graph: {}, /** * Reference to the `Axis` instance used for assigning * category values to the graph. * * @attribute categoryAxis * @type Axis */ categoryAxis: { value: null, validator: function(value) { return value !== this.get("categoryAxis"); } }, /** * Reference to the `Axis` instance used for assigning * series values to the graph. * * @attribute categoryAxis * @type Axis */ valueAxis: { value: null, validator: function(value) { return value !== this.get("valueAxis"); } }, /** * Indicates which array to from the hash of value arrays in * the category `Axis` instance. * * @attribute categoryKey * @type String */ categoryKey: { value: null, validator: function(value) { return value !== this.get("categoryKey"); } }, /** * Indicates which array to from the hash of value arrays in * the value `Axis` instance. * * @attribute valueKey * @type String */ valueKey: { value: null, validator: function(value) { return value !== this.get("valueKey"); } }, /** * Name used for for displaying category data * * @attribute categoryDisplayName * @type String */ categoryDisplayName: { setter: function(val) { this._categoryDisplayName = val; return val; }, getter: function() { return this._categoryDisplayName || this.get("categoryKey"); } }, /** * Name used for for displaying value data * * @attribute valueDisplayName * @type String */ valueDisplayName: { setter: function(val) { this._valueDisplayName = val; return val; }, getter: function() { return this._valueDisplayName || this.get("valueKey"); } }, /** * @attribute slices * @type Array * @private */ slices: null /** * Style properties used for drawing markers. This attribute is inherited from `MarkerSeries`. Below are the default * values: * <dl> * <dt>fill</dt><dd>A hash containing the following values: * <dl> * <dt>colors</dt><dd>An array of colors to be used for the marker fills. The color for each marker is * retrieved from the array below:<br/> * `["#66007f", "#a86f41", "#295454", "#996ab2", "#e8cdb7", "#90bdbd","#000000","#c3b8ca", "#968373", "#678585"]` * </dd> * <dt>alphas</dt><dd>An array of alpha references (Number from 0 to 1) indicating the opacity of each marker * fill. The default value is [1].</dd> * </dl> * </dd> * <dt>border</dt><dd>A hash containing the following values: * <dl> * <dt>color</dt><dd>An array of colors to be used for the marker borders. The color for each marker is * retrieved from the array below:<br/> * `["#205096", "#b38206", "#000000", "#94001e", "#9d6fa0", "#e55b00", "#5e85c9", "#adab9e", "#6ac291", "#006457"]` * <dt>alpha</dt><dd>Number from 0 to 1 indicating the opacity of the marker border. The default value is 1.</dd> * <dt>weight</dt><dd>Number indicating the width of the border. The default value is 1.</dd> * </dl> * </dd> * <dt>over</dt><dd>hash containing styles for markers when highlighted by a `mouseover` event. The default * values for each style is null. When an over style is not set, the non-over value will be used. For example, * the default value for `marker.over.fill.color` is equivalent to `marker.fill.color`.</dd> * </dl> * * @attribute styles * @type Object */ } });