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