/** * ChartLegend provides a legend for a chart. * * @class ChartLegend * @module charts * @submodule charts-legend * @extends Widget */ Y.ChartLegend = Y.Base.create("chartlegend", Y.Widget, [Y.Renderer], { /** * Initializes the chart. * * @method initializer * @private */ initializer: function() { this._items = []; }, /** * @method renderUI * @private */ renderUI: function() { var bb = this.get("boundingBox"), cb = this.get("contentBox"), styles = this.get("styles").background, background = new Y.Rect({ graphic: cb, fill: styles.fill, stroke: styles.border }); bb.setStyle("display", "block"); bb.setStyle("position", "absolute"); this.set("background", background); }, /** * @method bindUI * @private */ bindUI: function() { this.get("chart").after("seriesCollectionChange", Y.bind(this._updateHandler, this)); this.get("chart").after("stylesChange", Y.bind(this._updateHandler, this)); this.after("stylesChange", this._updateHandler); this.after("positionChange", this._positionChangeHandler); this.after("widthChange", this._handleSizeChange); this.after("heightChange", this._handleSizeChange); }, /** * @method syncUI * @private */ syncUI: function() { var w = this.get("width"), h = this.get("height"); if(isFinite(w) && isFinite(h) && w > 0 && h > 0) { this._drawLegend(); } }, /** * Handles changes to legend. * * @method _updateHandler * @param {Object} e Event object * @private */ _updateHandler: function() { if(this.get("rendered")) { this._drawLegend(); } }, /** * Handles position changes. * * @method _positionChangeHandler * @param {Object} e Event object * @private */ _positionChangeHandler: function() { var chart = this.get("chart"), parentNode = this._parentNode; if(parentNode && ((chart && this.get("includeInChartLayout")))) { this.fire("legendRendered"); } else if(this.get("rendered")) { this._drawLegend(); } }, /** * Updates the legend when the size changes. * * @method _handleSizeChange * @param {Object} e Event object. * @private */ _handleSizeChange: function(e) { var attrName = e.attrName, pos = this.get(POSITION), vert = pos === LEFT || pos === RIGHT, hor = pos === BOTTOM || pos === TOP; if((hor && attrName === WIDTH) || (vert && attrName === HEIGHT)) { this._drawLegend(); } }, /** * Draws the legend * * @method _drawLegend * @private */ _drawLegend: function() { if(this._drawing) { this._callLater = true; return; } this._drawing = true; this._callLater = false; if(this.get("includeInChartLayout")) { this.get("chart")._itemRenderQueue.unshift(this); } var chart = this.get("chart"), node = this.get("contentBox"), seriesCollection = chart.get("seriesCollection"), series, styles = this.get("styles"), padding = styles.padding, itemStyles = styles.item, seriesStyles, hSpacing = itemStyles.hSpacing, vSpacing = itemStyles.vSpacing, direction = this.get("direction"), align = direction === "vertical" ? styles.vAlign : styles.hAlign, marker = styles.marker, labelStyles = itemStyles.label, displayName, layout = this._layout[direction], i, len, isArray, legendShape, shape, shapeClass, item, fill, border, fillColors, borderColors, borderWeight, items = [], markerWidth = marker.width, markerHeight = marker.height, totalWidth = 0 - hSpacing, totalHeight = 0 - vSpacing, maxWidth = 0, maxHeight = 0, itemWidth, itemHeight; if(marker && marker.shape) { legendShape = marker.shape; } this._destroyLegendItems(); if(chart instanceof Y.PieChart) { series = seriesCollection[0]; displayName = series.get("categoryAxis").getDataByKey(series.get("categoryKey")); seriesStyles = series.get("styles").marker; fillColors = seriesStyles.fill.colors; borderColors = seriesStyles.border.colors; borderWeight = seriesStyles.border.weight; i = 0; len = displayName.length; shape = legendShape || Y.Circle; isArray = Y.Lang.isArray(shape); for(; i < len; ++i) { shape = isArray ? shape[i] : shape; fill = { color: fillColors[i] }; border = { colors: borderColors[i], weight: borderWeight }; displayName = chart.getSeriesItems(series, i).category.value; item = this._getLegendItem(node, this._getShapeClass(shape), fill, border, labelStyles, markerWidth, markerHeight, displayName); itemWidth = item.width; itemHeight = item.height; maxWidth = Math.max(maxWidth, itemWidth); maxHeight = Math.max(maxHeight, itemHeight); totalWidth += itemWidth + hSpacing; totalHeight += itemHeight + vSpacing; items.push(item); } } else { i = 0; len = seriesCollection.length; for(; i < len; ++i) { series = seriesCollection[i]; seriesStyles = this._getStylesBySeriesType(series, shape); if(!legendShape) { shape = seriesStyles.shape; if(!shape) { shape = Y.Circle; } } shapeClass = Y.Lang.isArray(shape) ? shape[i] : shape; item = this._getLegendItem( node, this._getShapeClass(shape), seriesStyles.fill, seriesStyles.border, labelStyles, markerWidth, markerHeight, series.get("valueDisplayName") ); itemWidth = item.width; itemHeight = item.height; maxWidth = Math.max(maxWidth, itemWidth); maxHeight = Math.max(maxHeight, itemHeight); totalWidth += itemWidth + hSpacing; totalHeight += itemHeight + vSpacing; items.push(item); } } this._drawing = false; if(this._callLater) { this._drawLegend(); } else { layout._positionLegendItems.apply( this, [items, maxWidth, maxHeight, totalWidth, totalHeight, padding, hSpacing, vSpacing, align] ); this._updateBackground(styles); this.fire("legendRendered"); } }, /** * Updates the background for the legend. * * @method _updateBackground * @param {Object} styles Reference to the legend's styles attribute * @private */ _updateBackground: function(styles) { var backgroundStyles = styles.background, contentRect = this._contentRect, padding = styles.padding, x = contentRect.left - padding.left, y = contentRect.top - padding.top, w = contentRect.right - x + padding.right, h = contentRect.bottom - y + padding.bottom; this.get("background").set({ fill: backgroundStyles.fill, stroke: backgroundStyles.border, width: w, height: h, x: x, y: y }); }, /** * Retrieves the marker styles based on the type of series. For series that contain a marker, the marker styles are returned. * * @method _getStylesBySeriesType * @param {CartesianSeries | PieSeries} The series in which the style properties will be received. * @return Object An object containing fill, border and shape information. * @private */ _getStylesBySeriesType: function(series) { var styles = series.get("styles"), color; if(series instanceof Y.LineSeries || series instanceof Y.StackedLineSeries) { styles = series.get("styles").line; color = styles.color || series._getDefaultColor(series.get("graphOrder"), "line"); return { border: { weight: 1, color: color }, fill: { color: color } }; } else if(series instanceof Y.AreaSeries || series instanceof Y.StackedAreaSeries) { styles = series.get("styles").area; color = styles.color || series._getDefaultColor(series.get("graphOrder"), "slice"); return { border: { weight: 1, color: color }, fill: { color: color } }; } else { styles = series.get("styles").marker; return { fill: styles.fill, border: { weight: styles.border.weight, color: styles.border.color, shape: styles.shape }, shape: styles.shape }; } }, /** * Returns a legend item consisting of the following properties: * <dl> * <dt>node</dt><dd>The `Node` containing the legend item elements.</dd> * <dt>shape</dt><dd>The `Shape` element for the legend item.</dd> * <dt>textNode</dt><dd>The `Node` containing the text></dd> * <dt>text</dt><dd></dd> * </dl> * * @method _getLegendItem * @param {Node} shapeProps Reference to the `node` attribute. * @param {String | Class} shapeClass The type of shape * @param {Object} fill Properties for the shape's fill * @param {Object} border Properties for the shape's border * @param {String} labelStyles String to be rendered as the legend's text * @param {Number} width Total width of the legend item * @param {Number} height Total height of the legend item * @param {String} text Text for the legendItem * @return Object * @private */ _getLegendItem: function(node, shapeClass, fill, border, labelStyles, w, h, text) { var containerNode = Y.Node.create("<div>"), textField = Y.Node.create("<span>"), shape, dimension, padding, left, item, ShapeClass = shapeClass; containerNode.setStyle(POSITION, "absolute"); textField.setStyle(POSITION, "absolute"); textField.setStyles(labelStyles); textField.set("text", text); containerNode.appendChild(textField); node.append(containerNode); dimension = textField.get("offsetHeight"); padding = dimension - h; left = w + padding + 2; textField.setStyle("left", left + PX); containerNode.setStyle("height", dimension + PX); containerNode.setStyle("width", (left + textField.get("offsetWidth")) + PX); shape = new ShapeClass({ fill: fill, stroke: border, width: w, height: h, x: padding * 0.5, y: padding * 0.5, w: w, h: h, graphic: containerNode }); textField.setStyle("left", dimension + PX); item = { node: containerNode, width: containerNode.get("offsetWidth"), height: containerNode.get("offsetHeight"), shape: shape, textNode: textField, text: text }; this._items.push(item); return item; }, /** * Evaluates and returns correct class for drawing a shape. * * @method _getShapeClass * @return Shape * @private */ _getShapeClass: function() { var graphic = this.get("background").get("graphic"); return graphic._getShapeClass.apply(graphic, arguments); }, /** * Returns the default hash for the `styles` attribute. * * @method _getDefaultStyles * @return Object * @protected */ _getDefaultStyles: function() { var styles = { padding: { top: 8, right: 8, bottom: 8, left: 9 }, gap: 10, hAlign: "center", vAlign: "top", marker: this._getPlotDefaults(), item: { hSpacing: 10, vSpacing: 5, label: { color:"#808080", fontSize:"85%", whiteSpace: "nowrap" } }, background: { shape: "rect", fill:{ color:"#faf9f2" }, border: { color:"#dad8c9", weight: 1 } } }; return styles; }, /** * Gets the default values for series that use the utility. This method is used by * the class' `styles` attribute's getter to get build default values. * * @method _getPlotDefaults * @return Object * @protected */ _getPlotDefaults: function() { var defs = { width: 10, height: 10 }; return defs; }, /** * Destroys legend items. * * @method _destroyLegendItems * @private */ _destroyLegendItems: function() { var item; if(this._items) { while(this._items.length > 0) { item = this._items.shift(); item.shape.get("graphic").destroy(); item.node.empty(); item.node.destroy(true); item.node = null; item = null; } } this._items = []; }, /** * Maps layout classes. * * @property _layout * @private */ _layout: { vertical: VerticalLegendLayout, horizontal: HorizontalLegendLayout }, /** * Destructor implementation ChartLegend class. Removes all items and the Graphic instance from the widget. * * @method destructor * @protected */ destructor: function() { var background = this.get("background"), backgroundGraphic; this._destroyLegendItems(); if(background) { backgroundGraphic = background.get("graphic"); if(backgroundGraphic) { backgroundGraphic.destroy(); } else { background.destroy(); } } } }, { ATTRS: { /** * Indicates whether the chart's contentBox is the parentNode for the legend. * * @attribute includeInChartLayout * @type Boolean * @private */ includeInChartLayout: { value: false }, /** * Reference to the `Chart` instance. * * @attribute chart * @type Chart */ chart: { setter: function(val) { this.after("legendRendered", Y.bind(val._itemRendered, val)); return val; } }, /** * Indicates the direction in relation of the legend's layout. The `direction` of the legend is determined by its * `position` value. * * @attribute direction * @type String */ direction: { value: "vertical" }, /** * Indicates the position and direction of the legend. Possible values are `left`, `top`, `right` and `bottom`. * Values of `left` and `right` values have a `direction` of `vertical`. Values of `top` and `bottom` values have * a `direction` of `horizontal`. * * @attribute position * @type String */ position: { lazyAdd: false, value: "right", setter: function(val) { if(val === TOP || val === BOTTOM) { this.set("direction", HORIZONTAL); } else if(val === LEFT || val === RIGHT) { this.set("direction", VERTICAL); } return val; } }, /** * The width of the legend. Depending on the implementation of the ChartLegend, this value is `readOnly`. * By default, the legend is included in the layout of the `Chart` that it references. Under this circumstance, * `width` is always `readOnly`. When the legend is rendered in its own dom element, the `readOnly` status is * determined by the direction of the legend. If the `position` is `left` or `right` or the `direction` is * `vertical`, width is `readOnly`. If the position is `top` or `bottom` or the `direction` is `horizontal`, * width can be explicitly set. If width is not explicitly set, the width will be determined by the width of the * legend's parent element. * * @attribute width * @type Number */ width: { getter: function() { var chart = this.get("chart"), parentNode = this._parentNode; if(parentNode) { if((chart && this.get("includeInChartLayout")) || this._width) { if(!this._width) { this._width = 0; } return this._width; } else { return parentNode.get("offsetWidth"); } } return ""; }, setter: function(val) { this._width = val; return val; } }, /** * The height of the legend. Depending on the implementation of the ChartLegend, this value is `readOnly`. * By default, the legend is included in the layout of the `Chart` that it references. Under this circumstance, * `height` is always `readOnly`. When the legend is rendered in its own dom element, the `readOnly` status is * determined by the direction of the legend. If the `position` is `top` or `bottom` or the `direction` is * `horizontal`, height is `readOnly`. If the position is `left` or `right` or the `direction` is `vertical`, * height can be explicitly set. If height is not explicitly set, the height will be determined by the width of the * legend's parent element. * * @attribute height * @type Number */ height: { valueFn: "_heightGetter", getter: function() { var chart = this.get("chart"), parentNode = this._parentNode; if(parentNode) { if((chart && this.get("includeInChartLayout")) || this._height) { if(!this._height) { this._height = 0; } return this._height; } else { return parentNode.get("offsetHeight"); } } return ""; }, setter: function(val) { this._height = val; return val; } }, /** * Indicates the x position of legend. * * @attribute x * @type Number * @readOnly */ x: { lazyAdd: false, value: 0, setter: function(val) { var node = this.get("boundingBox"); if(node) { node.setStyle(LEFT, val + PX); } return val; } }, /** * Indicates the y position of legend. * * @attribute y * @type Number * @readOnly */ y: { lazyAdd: false, value: 0, setter: function(val) { var node = this.get("boundingBox"); if(node) { node.setStyle(TOP, val + PX); } return val; } }, /** * Array of items contained in the legend. Each item is an object containing the following properties: * * <dl> * <dt>node</dt><dd>Node containing text for the legend item.</dd> * <dt>marker</dt><dd>Shape for the legend item.</dd> * </dl> * * @attribute items * @type Array * @readOnly */ items: { getter: function() { return this._items; } }, /** * Background for the legend. * * @attribute background * @type Rect */ background: {} /** * Properties used to display and style the ChartLegend. This attribute is inherited from `Renderer`. * Below are the default values: * * <dl> * <dt>gap</dt><dd>Distance, in pixels, between the `ChartLegend` instance and the chart's content. When `ChartLegend` * is rendered within a `Chart` instance this value is applied.</dd> * <dt>hAlign</dt><dd>Defines the horizontal alignment of the `items` in a `ChartLegend` rendered in a horizontal direction. * This value is applied when the instance's `position` is set to top or bottom. This attribute can be set to left, center * or right. The default value is center.</dd> * <dt>vAlign</dt><dd>Defines the vertical alignment of the `items` in a `ChartLegend` rendered in vertical direction. This * value is applied when the instance's `position` is set to left or right. The attribute can be set to top, middle or * bottom. The default value is middle.</dd> * <dt>item</dt><dd>Set of style properties applied to the `items` of the `ChartLegend`. * <dl> * <dt>hSpacing</dt><dd>Horizontal distance, in pixels, between legend `items`.</dd> * <dt>vSpacing</dt><dd>Vertical distance, in pixels, between legend `items`.</dd> * <dt>label</dt><dd>Properties for the text of an `item`. * <dl> * <dt>color</dt><dd>Color of the text. The default values is "#808080".</dd> * <dt>fontSize</dt><dd>Font size for the text. The default value is "85%".</dd> * </dl> * </dd> * <dt>marker</dt><dd>Properties for the `item` markers. * <dl> * <dt>width</dt><dd>Specifies the width of the markers.</dd> * <dt>height</dt><dd>Specifies the height of the markers.</dd> * </dl> * </dd> * </dl> * </dd> * <dt>background</dt><dd>Properties for the `ChartLegend` background. * <dl> * <dt>fill</dt><dd>Properties for the background fill. * <dl> * <dt>color</dt><dd>Color for the fill. The default value is "#faf9f2".</dd> * </dl> * </dd> * <dt>border</dt><dd>Properties for the background border. * <dl> * <dt>color</dt><dd>Color for the border. The default value is "#dad8c9".</dd> * <dt>weight</dt><dd>Weight of the border. The default values is 1.</dd> * </dl> * </dd> * </dl> * </dd> * </dl> * * @attribute styles * @type Object */ } });