/*
This file is part of Ext JS 4.2
Copyright (c) 2011-2013 Sencha Inc
Contact: http://www.sencha.com/contact
GNU General Public License Usage
This file may be used under the terms of the GNU General Public License version 3.0 as
published by the Free Software Foundation and appearing in the file LICENSE included in the
packaging of this file.
Please review the following information to ensure the GNU General Public License version 3.0
requirements will be met: http://www.gnu.org/copyleft/gpl.html.
If you are unsure which license is appropriate for your use, please contact the sales department
at http://www.sencha.com/contact.
Build date: 2013-05-16 14:36:50 (f9be68accb407158ba2b1be2c226a6ce1f649314)
*/
/**
* @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;
}
});
|