Upgrade to ExtJS 4.0.2 - Released 06/09/2011
[extjs.git] / src / chart / Legend.js
1 /*
2
3 This file is part of Ext JS 4
4
5 Copyright (c) 2011 Sencha Inc
6
7 Contact:  http://www.sencha.com/contact
8
9 GNU General Public License Usage
10 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.
11
12 If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
13
14 */
15 /**
16  * @class Ext.chart.Legend
17  *
18  * Defines a legend for a chart's series.
19  * The 'chart' member must be set prior to rendering.
20  * The legend class displays a list of legend items each of them related with a
21  * series being rendered. In order to render the legend item of the proper series
22  * the series configuration object must have `showInSeries` set to true.
23  *
24  * The legend configuration object accepts a `position` as parameter.
25  * The `position` parameter can be `left`, `right`
26  * `top` or `bottom`. For example:
27  *
28  *     legend: {
29  *         position: 'right'
30  *     },
31  * 
32  * Full example:
33     <pre><code>
34     var store = Ext.create('Ext.data.JsonStore', {
35         fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5'],
36         data: [
37             {'name':'metric one', 'data1':10, 'data2':12, 'data3':14, 'data4':8, 'data5':13},
38             {'name':'metric two', 'data1':7, 'data2':8, 'data3':16, 'data4':10, 'data5':3},
39             {'name':'metric three', 'data1':5, 'data2':2, 'data3':14, 'data4':12, 'data5':7},
40             {'name':'metric four', 'data1':2, 'data2':14, 'data3':6, 'data4':1, 'data5':23},
41             {'name':'metric five', 'data1':27, 'data2':38, 'data3':36, 'data4':13, 'data5':33}                                                
42         ]
43     });
44     
45     Ext.create('Ext.chart.Chart', {
46         renderTo: Ext.getBody(),
47         width: 500,
48         height: 300,
49         animate: true,
50         store: store,
51         shadow: true,
52         theme: 'Category1',
53         legend: {
54             position: 'top'
55         },
56          axes: [{
57                 type: 'Numeric',
58                 grid: true,
59                 position: 'left',
60                 fields: ['data1', 'data2', 'data3', 'data4', 'data5'],
61                 title: 'Sample Values',
62                 grid: {
63                     odd: {
64                         opacity: 1,
65                         fill: '#ddd',
66                         stroke: '#bbb',
67                         'stroke-width': 1
68                     }
69                 },
70                 minimum: 0,
71                 adjustMinimumByMajorUnit: 0
72             }, {
73                 type: 'Category',
74                 position: 'bottom',
75                 fields: ['name'],
76                 title: 'Sample Metrics',
77                 grid: true,
78                 label: {
79                     rotate: {
80                         degrees: 315
81                     }
82                 }
83         }],
84         series: [{
85             type: 'area',
86             highlight: false,
87             axis: 'left',
88             xField: 'name',
89             yField: ['data1', 'data2', 'data3', 'data4', 'data5'],
90             style: {
91                 opacity: 0.93
92             }
93         }]
94     });    
95     </code></pre>    
96  *
97  */
98 Ext.define('Ext.chart.Legend', {
99
100     /* Begin Definitions */
101
102     requires: ['Ext.chart.LegendItem'],
103
104     /* End Definitions */
105
106     /**
107      * @cfg {Boolean} visible
108      * Whether or not the legend should be displayed.
109      */
110     visible: true,
111
112     /**
113      * @cfg {String} position
114      * The position of the legend in relation to the chart. One of: "top",
115      * "bottom", "left", "right", or "float". If set to "float", then the legend
116      * box will be positioned at the point denoted by the x and y parameters.
117      */
118     position: 'bottom',
119
120     /**
121      * @cfg {Number} x
122      * X-position of the legend box. Used directly if position is set to "float", otherwise 
123      * it will be calculated dynamically.
124      */
125     x: 0,
126
127     /**
128      * @cfg {Number} y
129      * Y-position of the legend box. Used directly if position is set to "float", otherwise
130      * it will be calculated dynamically.
131      */
132     y: 0,
133
134     /**
135      * @cfg {String} labelFont
136      * Font to be used for the legend labels, eg '12px Helvetica'
137      */
138     labelFont: '12px Helvetica, sans-serif',
139
140     /**
141      * @cfg {String} boxStroke
142      * Style of the stroke for the legend box
143      */
144     boxStroke: '#000',
145
146     /**
147      * @cfg {String} boxStrokeWidth
148      * Width of the stroke for the legend box
149      */
150     boxStrokeWidth: 1,
151
152     /**
153      * @cfg {String} boxFill
154      * Fill style for the legend box
155      */
156     boxFill: '#FFF',
157
158     /**
159      * @cfg {Number} itemSpacing
160      * Amount of space between legend items
161      */
162     itemSpacing: 10,
163
164     /**
165      * @cfg {Number} padding
166      * Amount of padding between the legend box's border and its items
167      */
168     padding: 5,
169
170     // @private
171     width: 0,
172     // @private
173     height: 0,
174
175     /**
176      * @cfg {Number} boxZIndex
177      * Sets the z-index for the legend. Defaults to 100.
178      */
179     boxZIndex: 100,
180
181     /**
182      * Creates new Legend.
183      * @param {Object} config  (optional) Config object.
184      */
185     constructor: function(config) {
186         var me = this;
187         if (config) {
188             Ext.apply(me, config);
189         }
190         me.items = [];
191         /**
192          * Whether the legend box is oriented vertically, i.e. if it is on the left or right side or floating.
193          * @type {Boolean}
194          */
195         me.isVertical = ("left|right|float".indexOf(me.position) !== -1);
196         
197         // cache these here since they may get modified later on
198         me.origX = me.x;
199         me.origY = me.y;
200     },
201
202     /**
203      * @private Create all the sprites for the legend
204      */
205     create: function() {
206         var me = this;
207         me.createItems();
208         if (!me.created && me.isDisplayed()) {
209             me.createBox();
210             me.created = true;
211
212             // Listen for changes to series titles to trigger regeneration of the legend
213             me.chart.series.each(function(series) {
214                 series.on('titlechange', function() {
215                     me.create();
216                     me.updatePosition();
217                 });
218             });
219         }
220     },
221
222     /**
223      * @private Determine whether the legend should be displayed. Looks at the legend's 'visible' config,
224      * and also the 'showInLegend' config for each of the series.
225      */
226     isDisplayed: function() {
227         return this.visible && this.chart.series.findIndex('showInLegend', true) !== -1;
228     },
229
230     /**
231      * @private Create the series markers and labels
232      */
233     createItems: function() {
234         var me = this,
235             chart = me.chart,
236             surface = chart.surface,
237             items = me.items,
238             padding = me.padding,
239             itemSpacing = me.itemSpacing,
240             spacingOffset = 2,
241             maxWidth = 0,
242             maxHeight = 0,
243             totalWidth = 0,
244             totalHeight = 0,
245             vertical = me.isVertical,
246             math = Math,
247             mfloor = math.floor,
248             mmax = math.max,
249             index = 0, 
250             i = 0, 
251             len = items ? items.length : 0,
252             x, y, spacing, item, bbox, height, width;
253
254         //remove all legend items
255         if (len) {
256             for (; i < len; i++) {
257                 items[i].destroy();
258             }
259         }
260         //empty array
261         items.length = [];
262         // Create all the item labels, collecting their dimensions and positioning each one
263         // properly in relation to the previous item
264         chart.series.each(function(series, i) {
265             if (series.showInLegend) {
266                 Ext.each([].concat(series.yField), function(field, j) {
267                     item = Ext.create('Ext.chart.LegendItem', {
268                         legend: this,
269                         series: series,
270                         surface: chart.surface,
271                         yFieldIndex: j
272                     });
273                     bbox = item.getBBox();
274
275                     //always measure from x=0, since not all markers go all the way to the left
276                     width = bbox.width; 
277                     height = bbox.height;
278
279                     if (i + j === 0) {
280                         spacing = vertical ? padding + height / 2 : padding;
281                     }
282                     else {
283                         spacing = itemSpacing / (vertical ? 2 : 1);
284                     }
285                     // Set the item's position relative to the legend box
286                     item.x = mfloor(vertical ? padding : totalWidth + spacing);
287                     item.y = mfloor(vertical ? totalHeight + spacing : padding + height / 2);
288
289                     // Collect cumulative dimensions
290                     totalWidth += width + spacing;
291                     totalHeight += height + spacing;
292                     maxWidth = mmax(maxWidth, width);
293                     maxHeight = mmax(maxHeight, height);
294
295                     items.push(item);
296                 }, this);
297             }
298         }, me);
299
300         // Store the collected dimensions for later
301         me.width = mfloor((vertical ? maxWidth : totalWidth) + padding * 2);
302         if (vertical && items.length === 1) {
303             spacingOffset = 1;
304         }
305         me.height = mfloor((vertical ? totalHeight - spacingOffset * spacing : maxHeight) + (padding * 2));
306         me.itemHeight = maxHeight;
307     },
308
309     /**
310      * @private Get the bounds for the legend's outer box
311      */
312     getBBox: function() {
313         var me = this;
314         return {
315             x: Math.round(me.x) - me.boxStrokeWidth / 2,
316             y: Math.round(me.y) - me.boxStrokeWidth / 2,
317             width: me.width,
318             height: me.height
319         };
320     },
321
322     /**
323      * @private Create the box around the legend items
324      */
325     createBox: function() {
326         var me = this,
327             box = me.boxSprite = me.chart.surface.add(Ext.apply({
328                 type: 'rect',
329                 stroke: me.boxStroke,
330                 "stroke-width": me.boxStrokeWidth,
331                 fill: me.boxFill,
332                 zIndex: me.boxZIndex
333             }, me.getBBox()));
334         box.redraw();
335     },
336
337     /**
338      * @private Update the position of all the legend's sprites to match its current x/y values
339      */
340     updatePosition: function() {
341         var me = this,
342             x, y,
343             legendWidth = me.width,
344             legendHeight = me.height,
345             padding = me.padding,
346             chart = me.chart,
347             chartBBox = chart.chartBBox,
348             insets = chart.insetPadding,
349             chartWidth = chartBBox.width - (insets * 2),
350             chartHeight = chartBBox.height - (insets * 2),
351             chartX = chartBBox.x + insets,
352             chartY = chartBBox.y + insets,
353             surface = chart.surface,
354             mfloor = Math.floor;
355         
356         if (me.isDisplayed()) {
357             // Find the position based on the dimensions
358             switch(me.position) {
359                 case "left":
360                     x = insets;
361                     y = mfloor(chartY + chartHeight / 2 - legendHeight / 2);
362                     break;
363                 case "right":
364                     x = mfloor(surface.width - legendWidth) - insets;
365                     y = mfloor(chartY + chartHeight / 2 - legendHeight / 2);
366                     break;
367                 case "top":
368                     x = mfloor(chartX + chartWidth / 2 - legendWidth / 2);
369                     y = insets;
370                     break;
371                 case "bottom":
372                     x = mfloor(chartX + chartWidth / 2 - legendWidth / 2);
373                     y = mfloor(surface.height - legendHeight) - insets;
374                     break;
375                 default:
376                     x = mfloor(me.origX) + insets;
377                     y = mfloor(me.origY) + insets;
378             }
379             me.x = x;
380             me.y = y;
381
382             // Update the position of each item
383             Ext.each(me.items, function(item) {
384                 item.updatePosition();
385             });
386             // Update the position of the outer box
387             me.boxSprite.setAttributes(me.getBBox(), true);
388         }
389     }
390 });