/** * @class Ext.chart.Legend * * Defines a legend for a chart's series. * The 'chart' member must be set prior to rendering. * The legend class displays a list of legend items each of them related with a * series being rendered. In order to render the legend item of the proper series * the series configuration object must have `showInLegend` set to true. * * The legend configuration object accepts a `position` as parameter. * The `position` parameter can be `left`, `right` * `top` or `bottom`. For example: * * legend: { * position: 'right' * }, * * ## Example * * @example * var store = Ext.create('Ext.data.JsonStore', { * fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5'], * data: [ * { 'name': 'metric one', 'data1': 10, 'data2': 12, 'data3': 14, 'data4': 8, 'data5': 13 }, * { 'name': 'metric two', 'data1': 7, 'data2': 8, 'data3': 16, 'data4': 10, 'data5': 3 }, * { 'name': 'metric three', 'data1': 5, 'data2': 2, 'data3': 14, 'data4': 12, 'data5': 7 }, * { 'name': 'metric four', 'data1': 2, 'data2': 14, 'data3': 6, 'data4': 1, 'data5': 23 }, * { 'name': 'metric five', 'data1': 27, 'data2': 38, 'data3': 36, 'data4': 13, 'data5': 33 } * ] * }); * * Ext.create('Ext.chart.Chart', { * renderTo: Ext.getBody(), * width: 500, * height: 300, * animate: true, * store: store, * shadow: true, * theme: 'Category1', * legend: { * position: 'top' * }, * axes: [ * { * type: 'Numeric', * position: 'left', * fields: ['data1', 'data2', 'data3', 'data4', 'data5'], * title: 'Sample Values', * grid: { * odd: { * opacity: 1, * fill: '#ddd', * stroke: '#bbb', * 'stroke-width': 1 * } * }, * minimum: 0, * adjustMinimumByMajorUnit: 0 * }, * { * type: 'Category', * position: 'bottom', * fields: ['name'], * title: 'Sample Metrics', * grid: true, * label: { * rotate: { * degrees: 315 * } * } * } * ], * series: [{ * type: 'area', * highlight: false, * axis: 'left', * xField: 'name', * yField: ['data1', 'data2', 'data3', 'data4', 'data5'], * style: { * opacity: 0.93 * } * }] * }); */ Ext.define('Ext.chart.Legend', { /* Begin Definitions */ requires: ['Ext.chart.LegendItem'], /* End Definitions */ /** * @cfg {Boolean} visible * Whether or not the legend should be displayed. */ visible: true, /** * @cfg {Boolean} update * If set to true the legend will be refreshed when the chart is. * This is useful to update the legend items if series are * added/removed/updated from the chart. Default is true. */ update: true, /** * @cfg {String} position * The position of the legend in relation to the chart. One of: "top", * "bottom", "left", "right", or "float". If set to "float", then the legend * box will be positioned at the point denoted by the x and y parameters. */ position: 'bottom', /** * @cfg {Number} x * X-position of the legend box. Used directly if position is set to "float", otherwise * it will be calculated dynamically. */ x: 0, /** * @cfg {Number} y * Y-position of the legend box. Used directly if position is set to "float", otherwise * it will be calculated dynamically. */ y: 0, /** * @cfg {String} labelColor * Color to be used for the legend labels, eg '#000' */ labelColor: '#000', /** * @cfg {String} labelFont * Font to be used for the legend labels, eg '12px Helvetica' */ labelFont: '12px Helvetica, sans-serif', /** * @cfg {String} boxStroke * Style of the stroke for the legend box */ boxStroke: '#000', /** * @cfg {String} boxStrokeWidth * Width of the stroke for the legend box */ boxStrokeWidth: 1, /** * @cfg {String} boxFill * Fill style for the legend box */ boxFill: '#FFF', /** * @cfg {Number} itemSpacing * Amount of space between legend items */ itemSpacing: 10, /** * @cfg {Number} padding * Amount of padding between the legend box's border and its items */ padding: 5, // @private width: 0, // @private height: 0, /** * @cfg {Number} boxZIndex * Sets the z-index for the legend. Defaults to 100. */ boxZIndex: 100, /** * Creates new Legend. * @param {Object} config (optional) Config object. */ constructor: function(config) { var me = this; if (config) { Ext.apply(me, config); } me.items = []; /** * Whether the legend box is oriented vertically, i.e. if it is on the left or right side or floating. * @type {Boolean} */ me.isVertical = ("left|right|float".indexOf(me.position) !== -1); // cache these here since they may get modified later on me.origX = me.x; me.origY = me.y; }, /** * @private Create all the sprites for the legend */ create: function() { var me = this, seriesItems = me.chart.series.items, i, ln, series; me.createBox(); if (me.rebuild !== false) { me.createItems(); } if (!me.created && me.isDisplayed()) { me.created = true; // Listen for changes to series titles to trigger regeneration of the legend for (i = 0, ln = seriesItems.length; i < ln; i++) { series = seriesItems[i]; series.on('titlechange', me.redraw, me); } } }, /** * @private Redraws the Legend */ redraw: function() { var me = this; me.create(); me.updatePosition(); }, /** * @private Determine whether the legend should be displayed. Looks at the legend's 'visible' config, * and also the 'showInLegend' config for each of the series. */ isDisplayed: function() { return this.visible && this.chart.series.findIndex('showInLegend', true) !== -1; }, /** * @private Create the series markers and labels */ createItems: function() { var me = this, seriesItems = me.chart.series.items, items = me.items, fields, i, li, j, lj, series, item; //remove all legend items me.removeItems(); // Create all the item labels for (i = 0, li = seriesItems.length; i < li; i++) { series = seriesItems[i]; if (series.showInLegend) { fields = [].concat(series.yField); for (j = 0, lj = fields.length; j < lj; j++) { item = me.createLegendItem(series, j); items.push(item); } } } me.alignItems(); }, /** * @private Removes all legend items. */ removeItems: function() { var me = this, items = me.items, len = items ? items.length : 0, i; if (len) { for (i = 0; i < len; i++) { items[i].destroy(); } } //empty array items.length = []; }, /** * @private * Positions all items within Legend box. */ alignItems: function() { var me = this, padding = me.padding, vertical = me.isVertical, mfloor = Math.floor, dim, maxWidth, maxHeight, totalWidth, totalHeight; dim = me.updateItemDimensions(); maxWidth = dim.maxWidth; maxHeight = dim.maxHeight; totalWidth = dim.totalWidth; totalHeight = dim.totalHeight; // Store the collected dimensions for later me.width = mfloor((vertical ? maxWidth : totalWidth) + padding * 2); me.height = mfloor((vertical ? totalHeight : maxHeight) + padding * 2); }, updateItemDimensions: function() { var me = this, items = me.items, padding = me.padding, itemSpacing = me.itemSpacing, maxWidth = 0, maxHeight = 0, totalWidth = 0, totalHeight = 0, vertical = me.isVertical, mfloor = Math.floor, mmax = Math.max, spacing = 0, i, l, item, bbox, width, height; // Collect item dimensions and position each one // properly in relation to the previous item for (i = 0, l = items.length; i < l; i++) { item = items[i]; bbox = item.getBBox(); //always measure from x=0, since not all markers go all the way to the left width = bbox.width; height = bbox.height; spacing = (i === 0 ? 0 : itemSpacing); // Set the item's position relative to the legend box item.x = padding + mfloor(vertical ? 0 : totalWidth + spacing); item.y = padding + mfloor(vertical ? totalHeight + spacing : 0) + height / 2; // Collect cumulative dimensions totalWidth += spacing + width; totalHeight += spacing + height; maxWidth = mmax(maxWidth, width); maxHeight = mmax(maxHeight, height); } return { totalWidth: totalWidth, totalHeight: totalHeight, maxWidth: maxWidth, maxHeight: maxHeight }; }, /** * @private Creates single Legend Item */ createLegendItem: function(series, yFieldIndex) { var me = this; return new Ext.chart.LegendItem({ legend: me, series: series, surface: me.chart.surface, yFieldIndex: yFieldIndex }); }, /** * @private Get the bounds for the legend's outer box */ getBBox: function() { var me = this; return { x: Math.round(me.x) - me.boxStrokeWidth / 2, y: Math.round(me.y) - me.boxStrokeWidth / 2, width: me.width + me.boxStrokeWidth, height: me.height + me.boxStrokeWidth }; }, /** * @private Create the box around the legend items */ createBox: function() { var me = this, box, bbox; if (me.boxSprite) { me.boxSprite.destroy(); } bbox = me.getBBox(); //if some of the dimensions are NaN this means that we //cannot set a specific width/height for the legend //container. One possibility for this is that there are //actually no items to show in the legend, and the legend //should be hidden. if (isNaN(bbox.width) || isNaN(bbox.height)) { me.boxSprite = false; return; } box = me.boxSprite = me.chart.surface.add(Ext.apply({ type: 'rect', stroke: me.boxStroke, "stroke-width": me.boxStrokeWidth, fill: me.boxFill, zIndex: me.boxZIndex }, bbox)); box.redraw(); }, /** * @private Calculates Legend position with respect to other Chart elements. */ calcPosition: function() { var me = this, x, y, legendWidth = me.width, legendHeight = me.height, chart = me.chart, chartBBox = chart.chartBBox, insets = chart.insetPadding, chartWidth = chartBBox.width - (insets * 2), chartHeight = chartBBox.height - (insets * 2), chartX = chartBBox.x + insets, chartY = chartBBox.y + insets, surface = chart.surface, mfloor = Math.floor; // Find the position based on the dimensions switch(me.position) { case "left": x = insets; y = mfloor(chartY + chartHeight / 2 - legendHeight / 2); break; case "right": x = mfloor(surface.width - legendWidth) - insets; y = mfloor(chartY + chartHeight / 2 - legendHeight / 2); break; case "top": x = mfloor(chartX + chartWidth / 2 - legendWidth / 2); y = insets; break; case "bottom": x = mfloor(chartX + chartWidth / 2 - legendWidth / 2); y = mfloor(surface.height - legendHeight) - insets; break; default: x = mfloor(me.origX) + insets; y = mfloor(me.origY) + insets; } return { x: x, y: y }; }, /** * @private Update the position of all the legend's sprites to match its current x/y values */ updatePosition: function() { var me = this, items = me.items, pos, i, l, bbox; if (me.isDisplayed()) { // Find the position based on the dimensions pos = me.calcPosition(); me.x = pos.x; me.y = pos.y; // Update the position of each item for (i = 0, l = items.length; i < l; i++) { items[i].updatePosition(); } bbox = me.getBBox(); //if some of the dimensions are NaN this means that we //cannot set a specific width/height for the legend //container. One possibility for this is that there are //actually no items to show in the legend, and the legend //should be hidden. if (isNaN(bbox.width) || isNaN(bbox.height)) { if (me.boxSprite) { me.boxSprite.hide(true); } } else { if (!me.boxSprite) { me.createBox(); } // Update the position of the outer box me.boxSprite.setAttributes(bbox, true); me.boxSprite.show(true); } } }, /** toggle * @param {Boolean} show Whether to show or hide the legend. * */ toggle: function(show) { var me = this, i = 0, items = me.items, len = items.length; if (me.boxSprite) { if (show) { me.boxSprite.show(true); } else { me.boxSprite.hide(true); } } for (; i < len; ++i) { if (show) { items[i].show(true); } else { items[i].hide(true); } } me.visible = show; } });