Upgrade to ExtJS 3.3.1 - Released 11/30/2010
[extjs.git] / src / widgets / grid / PivotAxis.js
1 /*!
2  * Ext JS Library 3.3.1
3  * Copyright(c) 2006-2010 Sencha Inc.
4  * licensing@sencha.com
5  * http://www.sencha.com/license
6  */
7 /**
8  * @class Ext.grid.PivotAxis
9  * @extends Ext.Component
10  * <p>PivotAxis is a class that supports a {@link Ext.grid.PivotGrid}. Each PivotGrid contains two PivotAxis instances - the left
11  * axis and the top axis. Each PivotAxis defines an ordered set of dimensions, each of which should correspond to a field in a
12  * Store's Record (see {@link Ext.grid.PivotGrid} documentation for further explanation).</p>
13  * <p>Developers should have little interaction with the PivotAxis instances directly as most of their management is performed by
14  * the PivotGrid. An exception is the dynamic reconfiguration of axes at run time - to achieve this we use PivotAxis's 
15  * {@link #setDimensions} function and refresh the grid:</p>
16 <pre><code>
17 var pivotGrid = new Ext.grid.PivotGrid({
18     //some PivotGrid config here
19 });
20
21 //change the left axis dimensions
22 pivotGrid.leftAxis.setDimensions([
23     {
24         dataIndex: 'person',
25         direction: 'DESC',
26         width    : 100
27     },
28     {
29         dataIndex: 'product',
30         direction: 'ASC',
31         width    : 80
32     }
33 ]);
34
35 pivotGrid.view.refresh(true);
36 </code></pre>
37  * This clears the previous dimensions on the axis and redraws the grid with the new dimensions.
38  */
39 Ext.grid.PivotAxis = Ext.extend(Ext.Component, {
40     /**
41      * @cfg {String} orientation One of 'vertical' or 'horizontal'. Defaults to horizontal
42      */
43     orientation: 'horizontal',
44     
45     /**
46      * @cfg {Number} defaultHeaderWidth The width to render each row header that does not have a width specified via 
47      {@link #getRowGroupHeaders}. Defaults to 80.
48      */
49     defaultHeaderWidth: 80,
50     
51     /**
52      * @private
53      * @cfg {Number} paddingWidth The amount of padding used by each cell.
54      * TODO: From 4.x onwards this can be removed as it won't be needed. For now it is used to account for the differences between
55      * the content box and border box measurement models
56      */
57     paddingWidth: 7,
58     
59     /**
60      * Updates the dimensions used by this axis
61      * @param {Array} dimensions The new dimensions
62      */
63     setDimensions: function(dimensions) {
64         this.dimensions = dimensions;
65     },
66     
67     /**
68      * @private
69      * Builds the html table that contains the dimensions for this axis. This branches internally between vertical
70      * and horizontal orientations because the table structure is slightly different in each case
71      */
72     onRender: function(ct, position) {
73         var rows = this.orientation == 'horizontal'
74                  ? this.renderHorizontalRows()
75                  : this.renderVerticalRows();
76         
77         this.el = Ext.DomHelper.overwrite(ct.dom, {tag: 'table', cn: rows}, true);
78     },
79     
80     /**
81      * @private
82      * Specialised renderer for horizontal oriented axes
83      * @return {Object} The HTML Domspec for a horizontal oriented axis
84      */
85     renderHorizontalRows: function() {
86         var headers  = this.buildHeaders(),
87             rowCount = headers.length,
88             rows     = [],
89             cells, cols, colCount, i, j;
90         
91         for (i = 0; i < rowCount; i++) {
92             cells = [];
93             cols  = headers[i].items;
94             colCount = cols.length;
95
96             for (j = 0; j < colCount; j++) {
97                 cells.push({
98                     tag: 'td',
99                     html: cols[j].header,
100                     colspan: cols[j].span
101                 });
102             }
103
104             rows[i] = {
105                 tag: 'tr',
106                 cn: cells
107             };
108         }
109         
110         return rows;
111     },
112     
113     /**
114      * @private
115      * Specialised renderer for vertical oriented axes
116      * @return {Object} The HTML Domspec for a vertical oriented axis
117      */
118     renderVerticalRows: function() {
119         var headers  = this.buildHeaders(),
120             colCount = headers.length,
121             rowCells = [],
122             rows     = [],
123             rowCount, col, row, colWidth, i, j;
124         
125         for (i = 0; i < colCount; i++) {
126             col = headers[i];
127             colWidth = col.width || 80;
128             rowCount = col.items.length;
129             
130             for (j = 0; j < rowCount; j++) {
131                 row = col.items[j];
132                 
133                 rowCells[row.start] = rowCells[row.start] || [];
134                 rowCells[row.start].push({
135                     tag    : 'td',
136                     html   : row.header,
137                     rowspan: row.span,
138                     width  : Ext.isBorderBox ? colWidth : colWidth - this.paddingWidth
139                 });
140             }
141         }
142         
143         rowCount = rowCells.length;
144         for (i = 0; i < rowCount; i++) {
145             rows[i] = {
146                 tag: 'tr',
147                 cn : rowCells[i]
148             };
149         }
150         
151         return rows;
152     },
153     
154     /**
155      * @private
156      * Returns the set of all unique tuples based on the bound store and dimension definitions.
157      * Internally we construct a new, temporary store to make use of the multi-sort capabilities of Store. In
158      * 4.x this functionality should have been moved to MixedCollection so this step should not be needed.
159      * @return {Array} All unique tuples
160      */
161     getTuples: function() {
162         var newStore = new Ext.data.Store({});
163         
164         newStore.data = this.store.data.clone();
165         newStore.fields = this.store.fields;
166         
167         var sorters    = [],
168             dimensions = this.dimensions,
169             length     = dimensions.length,
170             i;
171         
172         for (i = 0; i < length; i++) {
173             sorters.push({
174                 field    : dimensions[i].dataIndex,
175                 direction: dimensions[i].direction || 'ASC'
176             });
177         }
178         
179         newStore.sort(sorters);
180         
181         var records = newStore.data.items,
182             hashes  = [],
183             tuples  = [],
184             recData, hash, info, data, key;
185         
186         length = records.length;
187         
188         for (i = 0; i < length; i++) {
189             info = this.getRecordInfo(records[i]);
190             data = info.data;
191             hash = "";
192             
193             for (key in data) {
194                 hash += data[key] + '---';
195             }
196             
197             if (hashes.indexOf(hash) == -1) {
198                 hashes.push(hash);
199                 tuples.push(info);
200             }
201         }
202         
203         newStore.destroy();
204         
205         return tuples;
206     },
207     
208     /**
209      * @private
210      */
211     getRecordInfo: function(record) {
212         var dimensions = this.dimensions,
213             length  = dimensions.length,
214             data    = {},
215             dimension, dataIndex, i;
216         
217         //get an object containing just the data we are interested in based on the configured dimensions
218         for (i = 0; i < length; i++) {
219             dimension = dimensions[i];
220             dataIndex = dimension.dataIndex;
221             
222             data[dataIndex] = record.get(dataIndex);
223         }
224         
225         //creates a specialised matcher function for a given tuple. The returned function will return
226         //true if the record passed to it matches the dataIndex values of each dimension in this axis
227         var createMatcherFunction = function(data) {
228             return function(record) {
229                 for (var dataIndex in data) {
230                     if (record.get(dataIndex) != data[dataIndex]) {
231                         return false;
232                     }
233                 }
234                 
235                 return true;
236             };
237         };
238         
239         return {
240             data: data,
241             matcher: createMatcherFunction(data)
242         };
243     },
244     
245     /**
246      * @private
247      * Uses the calculated set of tuples to build an array of headers that can be rendered into a table using rowspan or
248      * colspan. Basically this takes the set of tuples and spans any cells that run into one another, so if we had dimensions
249      * of Person and Product and several tuples containing different Products for the same Person, those Products would be
250      * spanned.
251      * @return {Array} The headers
252      */
253     buildHeaders: function() {
254         var tuples     = this.getTuples(),
255             rowCount   = tuples.length,
256             dimensions = this.dimensions,
257             colCount   = dimensions.length,
258             headers    = [],
259             tuple, rows, currentHeader, previousHeader, span, start, isLast, changed, i, j;
260         
261         for (i = 0; i < colCount; i++) {
262             dimension = dimensions[i];
263             rows  = [];
264             span  = 0;
265             start = 0;
266             
267             for (j = 0; j < rowCount; j++) {
268                 tuple  = tuples[j];
269                 isLast = j == (rowCount - 1);
270                 currentHeader = tuple.data[dimension.dataIndex];
271                 
272                 /*
273                  * 'changed' indicates that we need to create a new cell. This should be true whenever the cell
274                  * above (previousHeader) is different from this cell, or when the cell on the previous dimension
275                  * changed (e.g. if the current dimension is Product and the previous was Person, we need to start
276                  * a new cell if Product is the same but Person changed, so we check the previous dimension and tuple)
277                  */
278                 changed = previousHeader != undefined && previousHeader != currentHeader;
279                 if (i > 0 && j > 0) {
280                     changed = changed || tuple.data[dimensions[i-1].dataIndex] != tuples[j-1].data[dimensions[i-1].dataIndex];
281                 }
282                 
283                 if (changed) {                    
284                     rows.push({
285                         header: previousHeader,
286                         span  : span,
287                         start : start
288                     });
289                     
290                     start += span;
291                     span = 0;
292                 }
293                 
294                 if (isLast) {
295                     rows.push({
296                         header: currentHeader,
297                         span  : span + 1,
298                         start : start
299                     });
300                     
301                     start += span;
302                     span = 0;
303                 }
304                 
305                 previousHeader = currentHeader;
306                 span++;
307             }
308             
309             headers.push({
310                 items: rows,
311                 width: dimension.width || this.defaultHeaderWidth
312             });
313             
314             previousHeader = undefined;
315         }
316         
317         return headers;
318     }
319 });