Browse Source

big data grid

zhuth 5 years ago
parent
commit
5ede351152

+ 2 - 3
app/Application.js

@@ -8,15 +8,14 @@ Ext.define('uas.Application', {
     extend: 'Ext.app.Application',
 
     requires: [
-        'uas.view.main.*',
-        'uas.controller.Global',
-        'uas.model.Base'
+        'uas.*'
     ],
 
     name: 'uas',
 
     controllers: [
         'Global',
+        'Samples'
     ],
 
     defaultToken: '',

+ 8 - 29
app/controller/Global.js

@@ -40,36 +40,15 @@ Ext.define('uas.controller.Global', {
             //resume action
             action.resume();
         }else {
-            new Ext.Promise(function (resolve, reject) {
-                Ext.require(className,function(){
-                    resolve();
-                });
-            }).then(function(){
-                if(!!Ext.ClassManager.get(className)) {
-                    action.resume();
-                }else{
-                    Ext.Msg.alert(
-                        '创建组件失败',
-                        '确定以返回首页',
-                        function() {
-                            // TODO 路由跳转并不会引起页面刷新,待解决
-                            me.redirectTo(me.getApplication().getDefaultToken());
-                        }
-                    );
-                    //stop action
-                    action.stop();
+            Ext.Msg.alert(
+                '加载异常',
+                '确定以返回首页',
+                function() {
+                    // TODO 路由跳转并不会引起页面刷新,待解决
+                    me.redirectTo(me.getApplication().getDefaultToken());
                 }
-            }).catch(function(e){
-                Ext.Msg.alert(
-                    '加载异常',
-                    '确定以返回首页',
-                    function() {
-                        // TODO 路由跳转并不会引起页面刷新,待解决
-                        me.redirectTo(me.getApplication().getDefaultToken());
-                    }
-                );
-                action.stop();
-            })
+            );
+            action.stop();
         }
     },
 

+ 55 - 0
app/data/BigData.js

@@ -0,0 +1,55 @@
+Ext.define('uas.data.BigData', {
+    requires: [
+        'uas.data.Init'
+    ]
+}, function() {
+    function process(data) {
+        for (var i = 0; i < data.length; ++i) {
+            var d = data[i];
+            d.ratingLastYear = Math.max(Math.round(d.rating[0] / 2), 1);
+            d.ratingThisYear = Math.max(Math.round(d.rating[d.rating.length - 1] / 2), 1);
+        }
+        return data;
+    }
+
+    Ext.ux.ajax.SimManager.register({
+        '/uas/BigData': {
+            type: 'json',
+            data: process((function() {
+                let arr = [];
+                const foreNames = ["Nige","Jamie","Jay","Nicolas","Tommy","David","Ed","Dave","Abe","Adam","Aaron"];
+                const surNames = ["Mishcon","White","Davis","Kaneda","Spencer","Avins","Robinson","Elias","Conran","Ferrero","Maintz"];
+                const departments = ["Accounting","Administration","Engineering","Managment","Marketing","QA","Sales","Support"];
+
+                for(let i = 0; i < 30000; i++) {
+                    let forename = foreNames[Math.round(Math.random()*(foreNames.length - 1))];
+                    let surname = surNames[Math.round(Math.random()*(surNames.length - 1))];
+                    let dob = Ext.Date.add(new Date('1960-01-01'), 'd', Math.round(Math.random()*40 + 1)*365);
+                    let department = departments[Math.round(Math.random()*(departments.length - 1))];
+
+                    let obj = {
+                        employeeNo: Math.round(Math.random()*999999),
+                        rating: [Math.round(Math.random()*9), Math.round(Math.random()*9), Math.round(Math.random()*9), Math.round(Math.random()*9), Math.round(Math.random()*9),
+                            Math.round(Math.random()*9), Math.round(Math.random()*9), Math.round(Math.random()*9), Math.round(Math.random()*9), Math.round(Math.random()*9),
+                        ],
+                        salary: Math.round(Math.random()*8 + 1) * Math.pow(10, Math.round(Math.random()*3 + 2) ),
+                        forename: forename,
+                        surname: surname,
+                        email: forename.toLowerCase() + '.' + surname.toLowerCase() + '@sentcha.com',
+                        department: department,
+                        dob: Ext.util.Format.date(dob, 'Ymd'),
+                        joinDate: Ext.util.Format.date(Ext.Date.add(dob, Math.round(Math.random()*15 + 15)) * 365, 'Ymd'),
+                        sickDays: Math.round(Math.random()*10),
+                        holidayDays: Math.round(Math.random()*10),
+                        holidayAllowance: Math.round(Math.random()*20 + 20),
+                        noticePeriod: Math.round(Math.random()*2 + 1) + ['month', 'weeks'][Math.round(Math.random()*1)],
+                        avatar: "https://encrypted-tbn2.gstatic.com/images?q=tbn:ANd9GcSdj-gG2gXPkOUJGQ2r-3A5AnIgASv19axozeYMWssSVJyySvBIeQ",
+                        verified: Math.round(Math.random()) === 1
+                    };
+                    arr.push(obj);
+                }
+                return arr;
+            })())
+        }
+    });
+});

+ 88 - 0
app/model/Employee.js

@@ -0,0 +1,88 @@
+Ext.define('uas.model.Employee', {
+    extend: 'uas.model.Base',
+    fields: [{
+        name: 'employeeNo'
+    }, {
+        name: 'rating'
+    }, {
+        name: 'salary',
+        type: 'float'
+    }, {
+        name: 'forename'
+    }, {
+        name: 'surname'
+    }, {
+        name: 'name',
+        convert: function(v, rec) {
+            return rec.editing ? v : rec.get('forename') + ' ' + rec.get('surname');
+        }
+    }, {
+        name: 'email'
+    }, {
+        name: 'department'
+    }, {
+        name: 'dob',
+        type: 'date',
+        dateFormat: 'Ymd'
+    }, {
+        name: 'joinDate',
+        type: 'date',
+        dateFormat: 'Ymd'
+    }, {
+        name: 'noticePeriod'
+    }, {
+        name: 'sickDays',
+        type: 'int'
+    }, {
+        name: 'holidayDays',
+        type: 'int'
+    }, {
+        name: 'holidayAllowance',
+        type: 'int'
+    }, {
+        name: 'avatar'
+    }, {
+        name: 'ratingLastYear',
+        type: 'int'
+    }, {
+        name: 'ratingThisYear',
+        type: 'int'
+    }, {
+        name: 'active',
+        type: 'bool',
+        persist: false,
+        calculate: function() {
+            return !!(Math.random() >= 0.5);
+        }
+    }],
+    idField: 'employeeNo',
+
+    // Override set to update dependent fields
+    set: function (name, value) {
+        var data = name;
+
+        // Convert 2 arg form to object form
+        if (Ext.isString(name)) {
+            data = {};
+            data[name] = value;
+        }
+
+        // "name" is a calculated field, so update it on edit of "forename" or "surname".
+        if (data.forename || data.surname) {
+            data.name = (data.forename || this.get('forename')) + ' ' + (data.surname || this.get('surname'));
+        }
+        // Likewise, update two name fields if whole name gets updated
+        else if (data.name) {
+            var names = this.convertName(data.name);
+            data.forename = names[0];
+            data.surname = names[1];
+        }
+
+        return this.callParent([data]);
+    },
+
+    convertName: function(name) {
+        var names = /([^\s+]+)(?:\s+(.*))?/.exec(name);
+        return names ? [names[1], names[2]||''] : ['', ''];
+    }
+});

+ 18 - 0
app/store/BigData.js

@@ -0,0 +1,18 @@
+Ext.define('uas.store.BigData', {
+    extend: 'Ext.data.Store',
+    alias: 'store.big-data',
+
+    model: 'uas.model.Employee',
+
+    groupField: 'department',
+
+    proxy: {
+        type: 'ajax',
+        limitParam: null,
+        url: '/uas/BigData',
+        reader: {
+            type: 'json'
+        }
+    },
+    autoLoad: true
+});

+ 261 - 0
app/view/grid/bigData/Panel.js

@@ -0,0 +1,261 @@
+Ext.define('uas.view.grid.bigData.Panel', {
+    extend: 'Ext.grid.Panel',
+    xtype: 'big-data-grid',
+    controller: 'bigdata',
+
+    requires: [
+        'Ext.grid.filters.Filters',
+        'Ext.sparkline.Line',
+        'Ext.ux.rating.Picker'
+    ],
+
+    store: 'BigData',
+    columnLines: true,
+    multiColumnSort: true,
+
+    features: [{
+        ftype : 'groupingsummary',
+        groupHeaderTpl : '{name}',
+        hideGroupedHeader : false,
+        enableGroupingMenu : false
+    }, {
+        ftype: 'summary',
+        dock: 'bottom'
+    }],
+
+    layout: 'border',
+    split: true,
+
+    lockedGridConfig: {
+        title: 'Employees',
+        header: false,
+        collapsible: true,
+        width: 325,
+        minWidth: 290,
+        forceFit: true
+    },
+
+    selModel: {
+        type: 'checkboxmodel',
+        checkOnly: true
+    },
+
+    columns:[{
+        xtype: 'rownumberer',
+        width: 120,
+        sortable: false,
+        locked: true,
+        summaryType: 'count',
+        summaryRenderer: function(value, summaryData, dataIndex) {
+            return Ext.String.format('共有{0}条数据', value);
+        }
+    }, {
+        text: 'Id',
+        sortable: true,
+        dataIndex: 'employeeNo',
+        groupable: false,
+        width: 80,
+        locked: true,
+        editRenderer: 'bold'
+    }, {
+        text: 'Name (Filter)',
+        dataIndex: 'name',
+        sortable: true,
+        sorter: {
+            sorterFn: 'nameSorter' // set controller
+        },
+
+        width: 140,
+        groupable: false,
+
+        layout: 'hbox',
+        locked: true,
+        renderer: 'concatNames',
+        editor: {
+            xtype: 'textfield'
+        },
+        items: {
+            xtype: 'textfield',
+            fieldStyle: "",
+            reference: 'nameFilterField',
+            flex : 1,
+            margin: 2,
+            enableKeyEvents: true,
+            listeners: {
+                keyup: 'onNameFilterKeyup',
+                buffer: 500
+            }
+        }
+    }, {
+        text: 'Rating',
+        width: 100,
+        sortable: true,
+        dataIndex: 'rating',
+        groupable: false,
+        xtype: 'widgetcolumn',
+        widget: {
+            xtype: 'sparklineline'
+        }
+    }, {
+        text: 'Date of birth',
+        dataIndex: 'dob',
+        xtype: 'datecolumn',
+        groupable: false,
+        width: 115,
+        filter: {
+
+        },
+        editor: {
+            xtype: 'datefield'
+        },
+        exportStyle: {
+            alignment: {
+                horizontal: 'Right'
+            },
+            format: 'Long Date'
+        }
+    }, {
+        text: 'Join date',
+        dataIndex: 'joinDate',
+        xtype: 'datecolumn',
+        groupable: false,
+        width: 120,
+        filter: {
+
+        },
+        editor: {
+            xtype: 'datefield'
+        },
+        exportStyle: {
+            alignment: {
+                horizontal: 'Right'
+            },
+            format: 'Long Date'
+        }
+    }, {
+        text: 'Notice<br>period',
+        dataIndex: 'noticePeriod',
+        groupable: false,
+        width: 115,
+        filter: {
+            type: 'list'
+        },
+        editor: {
+            xtype: 'combobox',
+            listeners: {
+                beforerender: 'onBeforeRenderNoticeEditor'
+            }
+        }
+    }, {
+        text: 'Email address',
+        dataIndex: 'email',
+
+        width: 200,
+        groupable: false,
+        renderer: 'renderMailto',
+        editor: {
+            xtype: 'textfield'
+        }
+    }, {
+        text: 'Department',
+        dataIndex: 'department',
+        hidden: true,
+        hideable: false,
+        filter: {
+            type: 'list'
+        }
+    }, {
+        text: 'Absences',
+        shrinkWrap: true,
+        columns: [{
+            text: 'Illness',
+            dataIndex: 'sickDays',
+            width: 100,
+            groupable: false,
+            summaryType: 'sum',
+            summaryFormatter: 'number("0")',
+            filter: {
+
+            },
+            editor: {
+                xtype: 'numberfield',
+                decimalPrecision: 0
+            }
+        }, {
+            text: 'Holidays',
+            dataIndex: 'holidayDays',
+            width: null, // Size column to title text
+            groupable: false,
+            summaryType: 'sum',
+            summaryFormatter: 'number("0")',
+            filter: {
+
+            },
+            editor: {
+                xtype: 'numberfield',
+                decimalPrecision: 0
+            }
+        }, {
+            text: 'Holiday Allowance',
+            dataIndex: 'holidayAllowance',
+            width: null, // Size column to title text
+            groupable: false,
+            summaryType: 'sum',
+            summaryFormatter: 'number("0.00")',
+            formatter: 'number("0.00")',
+            filter: {
+
+            },
+            editor: {
+                xtype: 'numberfield',
+                decimalPrecision: 0
+            }
+        }]
+    }, {
+        text: 'Rating<br>This Year',
+        dataIndex: 'ratingThisYear',
+        groupable: false,
+        xtype: 'widgetcolumn',
+        widget: {
+            xtype: 'rating',
+            tip: 'Set to {tracking:plural("Star")}'
+        }
+    }, {
+        text: 'Salary',
+        width: 155,
+        sortable: true,
+        dataIndex: 'salary',
+        align: 'right',
+        formatter: 'usMoney',
+        groupable: false,
+        summaryType: 'average',
+        summaryFormatter: 'usMoney',
+        filter: {
+
+        },
+        editor: {
+            xtype: 'numberfield',
+            decimalPrecision: 2
+        },
+        exportStyle: {
+            alignment: {
+                horizontal: 'Right'
+            },
+            format: 'Currency'
+        }
+    }],
+
+    viewConfig: {
+        stripeRows: true
+    },
+
+    plugins: {
+        gridfilters: true,
+        rowexpander: {
+            // dblclick invokes the row editor
+            expandOnDblClick: false,
+            rowBodyTpl: '<img src="{avatar}" height="100px" '+
+                'style="float:left;margin:0 10px 5px 0"><b>{name}<br></b>{dob:date}'
+        }
+    }
+});

+ 83 - 0
app/view/grid/bigData/PanelController.js

@@ -0,0 +1,83 @@
+/**
+ * The Controller for the BigData view.
+ *
+ * Provides logic which is referenced by listeners, handlers and renderers in the view which are configured
+ * as strings. They are resolved to members of this class.
+ * 
+ */
+Ext.define('uas.view.grid.bigData.PanelController', {
+    extend: 'Ext.app.ViewController',
+    alias: 'controller.bigdata',
+
+    init: function() {
+        // RowEditing not appropriate for touch devices
+        if (!Ext.supports.Touch) {
+            // Plugins are instantiated at this time, we must add an instantiated Plugin, not a config
+            this.getView().getPlugins().push(Ext.create({
+                xclass: 'Ext.grid.plugin.RowEditing',
+                clicksToMoveEditor: 1,
+                autoCancel: false
+            }));
+        }
+    },
+
+    // Used as a column renderer by BigData: resolved using defaultListenerScope
+    concatNames: function(v, cellValues, rec) {
+        return rec.get('forename') + ' ' + rec.get('surname');
+    },
+
+    // Used as an editRenderer by BigData to display an uneditable field in the RowEditor
+    bold: function(v) {
+        return "<b>" + v + "</b>";
+    },
+
+    nameSorter: function (rec1, rec2) {
+        // Sort prioritizing surname over forename as would be expected.
+        var rec1Name = rec1.get('surname') + rec1.get('forename'),
+            rec2Name = rec2.get('surname') + rec2.get('forename');
+
+        if (rec1Name > rec2Name) {
+            return 1;
+        }
+        if (rec1Name < rec2Name) {
+            return -1;
+        }
+        return 0;
+    },
+
+    onBeforeRenderNoticeEditor: function (editor) {
+        var view = this.getView(),
+            store = view.store;
+
+        editor.setStore(store.collect('noticePeriod', false, true));
+    },
+
+    onNameFilterKeyup: function() {
+        var grid = this.getView(),
+            // Access the field using its "reference" property name.
+            filterField = this.lookupReference('nameFilterField'),
+            filters = grid.store.getFilters();
+
+        if (filterField.value) {
+            this.nameFilter = filters.add({
+                id            : 'nameFilter',
+                property      : 'name',
+                value         : filterField.value,
+                anyMatch      : true,
+                caseSensitive : false
+            });
+        } else if (this.nameFilter) {
+            filters.remove(this.nameFilter);
+            this.nameFilter = null;
+        }
+    },
+
+    onShowHeadersToggle: function(checkItem, checked) {
+        this.getView().setHeaderBorders(checked);
+    },
+
+    renderMailto: function (v) {
+        return '<a href="mailto:' + encodeURIComponent(v) + '">' + 
+            Ext.htmlEncode(v) + '</a>';
+    },
+});

+ 0 - 2
app/view/grid/dataList/DataListPanel.js

@@ -8,8 +8,6 @@ Ext.define('uas.view.grid.dataList.DataListPanel', {
     extend: 'Ext.grid.Panel',
     xtype: 'dataListPanel',
     requires: [
-        'uas.view.plugins.gridHeaderFilter.GridHeaderFilter',
-        'uas.store.DataListGridStore'
     ],
 
     plugins: {

+ 0 - 3
app/view/grid/expander/Panel.js

@@ -11,9 +11,6 @@ Ext.define('uas.view.grid.expander.Panel', {
     xtype: 'grid-expander-panel',
 
     requires: [
-        'uas.data.Company',
-        'uas.model.Company',
-        'uas.store.Companies'
     ],
 
     bind: '{companies}',

+ 0 - 3
app/view/grid/grouped/Panel.js

@@ -7,9 +7,6 @@ Ext.define('uas.view.grid.grouped.Panel', {
     controller: 'grouped-grid',
 
     requires: [
-        'uas.view.grid.grouped.PanelController',
-        'uas.model.Restaurant',
-        'uas.store.Restaurants'
     ],
 
     bind: '{restaurants}',

+ 1 - 4
app/view/grid/paging/Panel.js

@@ -4,10 +4,7 @@ Ext.define('uas.view.grid.paging.Panel', {
     controller: 'basic-grid',
 
     requires: [
-        'uas.view.grid.basic.PanelController',
-        'uas.data.Company',
-        'uas.model.Company',
-        'uas.store.Companies'
+        'Ext.toolbar.Paging'
     ],
 
     store: {

+ 0 - 3
app/view/grid/summary/Panel.js

@@ -3,9 +3,6 @@ Ext.define('uas.view.grid.summary.Panel', {
     xtype: 'grid-summary-panel',
 
     requires: [
-        'uas.view.grid.summary.Summary1',
-        'uas.view.grid.summary.Summary2',
-        'uas.view.grid.summary.Summary3'
     ],
 
     items: [{

+ 1 - 4
app/view/grid/summary/Summary1.js

@@ -7,10 +7,7 @@ Ext.define('uas.view.grid.summary.Summary1', {
     xtype: 'summary1',
 
     requires: [
-        'Ext.grid.feature.Summary',
-        'uas.data.Company',
-        'uas.model.Company',
-        'uas.store.Companies'
+        'Ext.grid.feature.Summary'
     ],
 
     features: [{

+ 1 - 4
app/view/grid/summary/Summary2.js

@@ -7,10 +7,7 @@ Ext.define('uas.view.grid.summary.Summary2', {
     xtype: 'summary2',
 
     requires: [
-        'Ext.grid.feature.Summary',
-        'uas.data.Company',
-        'uas.model.Company',
-        'uas.store.Companies'
+        'Ext.grid.feature.Summary'
     ],
 
     features: [{

+ 1 - 4
app/view/grid/summary/summary3.js

@@ -7,10 +7,7 @@ Ext.define('uas.view.grid.summary.Summary3', {
     controller: 'basic-grid',
 
     requires: [
-        'uas.view.grid.basic.PanelController',
-        'uas.data.Company',
-        'uas.model.Company',
-        'uas.store.Companies'
+        'Ext.grid.feature.Summary'
     ],
 
     features: [{

+ 6 - 0
resources/json/navigation.json

@@ -76,6 +76,12 @@
                 "leaf": true,
                 "target": "paging-grid",
                 "iconCls": "x-fa fa-smile-o"
+            },
+            {
+                "text": "大数据",
+                "leaf": true,
+                "target": "big-data-grid",
+                "iconCls": "x-fa fa-smile-o"
             }
         ]
     },

+ 10 - 0
samples/controller/Samples.js

@@ -0,0 +1,10 @@
+Ext.define('uas.controller.Samples', {
+    extend: 'Ext.app.Controller',
+    namespace: 'uas',
+
+    stores: [
+        'Companies',
+        'Restaurants',
+        'BigData',
+    ],
+});

+ 556 - 0
ux/rating/Picker.js

@@ -0,0 +1,556 @@
+/**
+ * A ratings picker based on `Ext.Gadget`.
+ *
+ *      @example
+ *      Ext.create({
+    *          xtype: 'rating',
+    *          renderTo: Ext.getBody(),
+    *          listeners: {
+    *              change: function (picker, value) {
+    *                 console.log('Rating ' + value);
+    *              }
+    *          }
+    *      });
+    */
+   Ext.define('Ext.ux.rating.Picker', {
+       extend: 'Ext.Gadget',
+    
+       xtype: 'rating',
+    
+       focusable: true,
+    
+       /*
+        * The "cachedConfig" block is basically the same as "config" except that these
+        * values are applied specially to the first instance of the class. After processing
+        * these configs, the resulting values are stored on the class `prototype` and the
+        * template DOM element also reflects these default values.
+        */
+       cachedConfig: {
+           /**
+            * @cfg {String} [family]
+            * The CSS `font-family` to use for displaying the `{@link #glyphs}`.
+            */
+           family: 'monospace',
+    
+           /**
+            * @cfg {String/String[]/Number[]} [glyphs]
+            * Either a string containing the two glyph characters, or an array of two strings
+            * containing the individual glyph characters or an array of two numbers with the
+            * character codes for the individual glyphs.
+            *
+            * For example:
+            *
+            *      @example
+            *      Ext.create({
+            *          xtype: 'rating',
+            *          renderTo: Ext.getBody(),
+            *          glyphs: [ 9671, 9670 ], // '◇◆',
+            *          listeners: {
+            *              change: function (picker, value) {
+            *                 console.log('Rating ' + value);
+            *              }
+            *          }
+            *      });
+            */
+           glyphs: '☆★',
+    
+           /**
+            * @cfg {Number} [minimum=1]
+            * The minimum allowed `{@link #value}` (rating).
+            */
+           minimum: 1,
+    
+           /**
+            * @cfg {Number} [limit]
+            * The maximum allowed `{@link #value}` (rating).
+            */
+           limit: 5,
+    
+           /**
+            * @cfg {String/Object} [overStyle]
+            * Optional styles to apply to the rating glyphs when `{@link #trackOver}` is
+            * enabled.
+            */
+           overStyle: null,
+    
+           /**
+            * @cfg {Number} [rounding=1]
+            * The rounding to apply to values. Common choices are 0.5 (for half-steps) or
+            * 0.25 (for quarter steps).
+            */
+           rounding: 1,
+    
+           /**
+            * @cfg {String} [scale="125%"]
+            * The CSS `font-size` to apply to the glyphs. This value defaults to 125% because
+            * glyphs in the stock font tend to be too small. When using specially designed
+            * "icon fonts" you may want to set this to 100%.
+            */
+           scale: '125%',
+    
+           /**
+            * @cfg {String/Object} [selectedStyle]
+            * Optional styles to apply to the rating value glyphs.
+            */
+           selectedStyle: null,
+    
+           /**
+            * @cfg {Object/String/String[]/Ext.XTemplate/Function} tip
+            * A template or a function that produces the tooltip text. The `Object`, `String`
+            * and `String[]` forms are converted to an `Ext.XTemplate`. If a function is given,
+            * it will be called with an object parameter and should return the tooltip text.
+            * The object contains these properties:
+            *
+            *   - component: The rating component requesting the tooltip.
+            *   - tracking: The current value under the mouse cursor.
+            *   - trackOver: The value of the `{@link #trackOver}` config.
+            *   - value: The current value.
+            *
+            * Templates can use these properties to generate the proper text.
+            */
+           tip: null,
+    
+           /**
+            * @cfg {Boolean} [trackOver=true]
+            * Determines if mouse movements should temporarily update the displayed value.
+            * The actual `value` is only updated on `click` but this rather acts as the
+            * "preview" of the value prior to click.
+            */
+           trackOver: true,
+    
+           /**
+            * @cfg {Number} value 
+            * The rating value. This value is bounded by `minimum` and `limit` and is also
+            * adjusted by the `rounding`.
+            */
+           value: null,
+    
+           //--------------------------------------------------------------------- 
+           // Private configs 
+    
+           /**
+            * @cfg {String} tooltipText 
+            * The current tooltip text. This value is set into the DOM by the updater (hence
+            * only when it changes). This is intended for use by the tip manager
+            * (`{@link Ext.tip.QuickTipManager}`). Developers should never need to set this
+            * config since it is handled by virtue of setting other configs (such as the
+            * {@link #tooltip} or the {@link #value}.).
+            * @private
+            */
+           tooltipText: null,
+    
+           /**
+            * @cfg {Number} trackingValue 
+            * This config is used to when `trackOver` is `true` and represents the tracked
+            * value. This config is maintained by our `mousemove` handler. This should not
+            * need to be set directly by user code.
+            * @private
+            */
+           trackingValue: null
+       },
+    
+       config: {
+           /**
+            * @cfg {Boolean/Object} [animate=false]
+            * Specifies an animation to use when changing the `{@link #value}`. When setting
+            * this config, it is probably best to set `{@link #trackOver}` to `false`.
+            */
+           animate: null
+       },
+    
+       // This object describes our element tree from the root. 
+       element: {
+           cls: 'u' + Ext.baseCSSPrefix + 'rating-picker',
+    
+           // Since we are replacing the entire "element" tree, we have to assign this 
+           // "reference" as would our base class. 
+           reference: 'element',
+    
+           children: [{
+               reference: 'innerEl',
+               cls: 'u' + Ext.baseCSSPrefix + 'rating-picker-inner',
+               listeners: {
+                   click: 'onClick',
+                   mousemove: 'onMouseMove',
+                   mouseenter: 'onMouseEnter',
+                   mouseleave: 'onMouseLeave'
+               },
+    
+               children: [{
+                   reference: 'valueEl',
+                   cls: 'u' + Ext.baseCSSPrefix + 'rating-picker-value'
+               },{
+                   reference: 'trackerEl',
+                   cls: 'u' + Ext.baseCSSPrefix + 'rating-picker-tracker'
+               }]
+           }]
+       },
+    
+       // Tell the Binding system to default to our "value" config. 
+       defaultBindProperty: 'value',
+    
+       // Enable two-way data binding for the "value" config. 
+       twoWayBindable: 'value',
+    
+       overCls: 'u' + Ext.baseCSSPrefix + 'rating-picker-over',
+    
+       trackOverCls: 'u' + Ext.baseCSSPrefix + 'rating-picker-track-over',
+    
+       //------------------------------------------------------------------------- 
+       // Config Appliers 
+    
+       applyGlyphs: function (value) {
+           if (typeof value === 'string') {
+               //<debug> 
+               if (value.length !== 2) {
+                   Ext.raise('Expected 2 characters for "glyphs" not "' + value +'".');
+               }
+               //</debug> 
+               value = [ value.charAt(0), value.charAt(1) ];
+           }
+           else if (typeof value[0] === 'number') {
+               value = [
+                   String.fromCharCode(value[0]),
+                   String.fromCharCode(value[1])
+               ];
+           }
+    
+           return value;
+       },
+    
+       applyOverStyle: function(style) {
+           this.trackerEl.applyStyles(style);
+       },
+    
+       applySelectedStyle: function(style) {
+           this.valueEl.applyStyles(style);
+       },
+    
+       applyTip: function (tip) {
+           if (tip && typeof tip !== 'function') {
+               if (!tip.isTemplate) {
+                   tip = new Ext.XTemplate(tip);
+               }
+    
+               tip = tip.apply.bind(tip);
+           }
+    
+           return tip;
+       },
+    
+       applyTrackingValue: function (value) {
+           return this.applyValue(value); // same rounding as normal value 
+       },
+    
+       applyValue: function (v) {
+           if (v !== null) {
+               var rounding = this.getRounding(),
+                   limit = this.getLimit(),
+                   min = this.getMinimum();
+    
+               v = Math.round(Math.round(v / rounding) * rounding * 1000) / 1000;
+               v = (v < min) ? min : (v > limit ? limit : v);
+           }
+    
+           return v;
+       },
+    
+       //------------------------------------------------------------------------- 
+       // Event Handlers 
+    
+       onClick: function (event) {
+           var value = this.valueFromEvent(event);
+           this.setValue(value);
+       },
+    
+       onMouseEnter: function () {
+           this.element.addCls(this.overCls);
+       },
+    
+       onMouseLeave: function () {
+           this.element.removeCls(this.overCls);
+       },
+    
+       onMouseMove: function (event) {
+           var value = this.valueFromEvent(event);
+           this.setTrackingValue(value);
+       },
+    
+       //------------------------------------------------------------------------- 
+       // Config Updaters 
+    
+       updateFamily: function (family) {
+           this.element.setStyle('fontFamily', "'" + family + "'");
+       },
+    
+       updateGlyphs: function () {
+           this.refreshGlyphs();
+       },
+    
+       updateLimit: function () {
+           this.refreshGlyphs();
+       },
+    
+       updateScale: function (size) {
+           this.element.setStyle('fontSize', size);
+       },
+    
+       updateTip: function () {
+           this.refreshTip();
+       },
+    
+       updateTooltipText: function (text) {
+           this.setTooltip(text);  // modern only (replaced by classic override) 
+       },
+    
+       updateTrackingValue: function (value) {
+           var me = this,
+               trackerEl = me.trackerEl,
+               newWidth = me.valueToPercent(value);
+    
+           trackerEl.setStyle('width', newWidth);
+    
+           me.refreshTip();
+       },
+    
+       updateTrackOver: function (trackOver) {
+           this.element.toggleCls(this.trackOverCls, trackOver);
+       },
+    
+       updateValue: function (value, oldValue) {
+           var me = this,
+               animate = me.getAnimate(),
+               valueEl = me.valueEl,
+               newWidth = me.valueToPercent(value),
+               column, record;
+    
+           if (me.isConfiguring || !animate) {
+               valueEl.setStyle('width', newWidth);
+           } else {
+               valueEl.stopAnimation();
+               valueEl.animate(Ext.merge({
+                   from: { width: me.valueToPercent(oldValue) },
+                   to:   { width: newWidth }
+               }, animate));
+           }
+    
+           me.refreshTip();
+    
+           if (!me.isConfiguring) {
+               // Since we are (re)configured many times as we are used in a grid cell, we 
+               // avoid firing the change event unless there are listeners. 
+               if (me.hasListeners.change) {
+                   me.fireEvent('change', me, value, oldValue);
+               }
+    
+               column = me.getWidgetColumn && me.getWidgetColumn();
+               record = column && me.getWidgetRecord && me.getWidgetRecord();
+    
+               if (record && column.dataIndex) {
+                   // When used in a widgetcolumn, we should update the backing field. The 
+                   // linkages will be cleared as we are being recycled, so this will only 
+                   // reach this line when we are properly attached to a record and the 
+                   // change is coming from the user (or a call to setValue). 
+                   record.set(column.dataIndex, value);
+               }
+           }
+       },
+    
+       //------------------------------------------------------------------------- 
+       // Config System Optimizations 
+       // 
+       // These are to deal with configs that combine to determine what should be 
+       // rendered in the DOM. For example, "glyphs" and "limit" must both be known 
+       // to render the proper text nodes. The "tip" and "value" likewise are 
+       // used to update the tooltipText. 
+       // 
+       // To avoid multiple updates to the DOM (one for each config), we simply mark 
+       // the rendering as invalid and post-process these flags on the tail of any 
+       // bulk updates. 
+    
+       afterCachedConfig: function () {
+           // Now that we are done setting up the initial values we need to refresh the 
+           // DOM before we allow Ext.Widget's implementation to cloneNode on it. 
+           this.refresh();
+    
+           return this.callParent(arguments);
+       },
+    
+       initConfig: function (instanceConfig) {
+           this.isConfiguring = true;
+    
+           this.callParent([ instanceConfig ]);
+    
+           // The firstInstance will already have refreshed the DOM (in afterCacheConfig) 
+           // but all instances beyond the first need to refresh if they have custom values 
+           // for one or more configs that affect the DOM (such as "glyphs" and "limit"). 
+           this.refresh();
+       },
+    
+       setConfig: function () {
+           var me = this;
+    
+           // Since we could be updating multiple configs, save any updates that need 
+           // multiple values for afterwards. 
+           me.isReconfiguring = true;
+    
+           me.callParent(arguments);
+    
+           me.isReconfiguring = false;
+    
+           // Now that all new values are set, we can refresh the DOM. 
+           me.refresh();
+    
+           return me;
+       },
+    
+       //------------------------------------------------------------------------- 
+    
+       privates: {
+           /**
+            * This method returns the DOM text node into which glyphs are placed.
+            * @param {HTMLElement} dom The DOM node parent of the text node.
+            * @return {HTMLElement} The text node.
+            * @private
+            */
+           getGlyphTextNode: function (dom) {
+               var node = dom.lastChild;
+    
+               // We want all our text nodes to be at the end of the child list, most 
+               // especially the text node on the innerEl. That text node affects the 
+               // default left/right position of our absolutely positioned child divs 
+               // (trackerEl and valueEl). 
+    
+               if (!node || node.nodeType !== 3) {
+                   node = dom.ownerDocument.createTextNode('');
+                   dom.appendChild(node);
+               }
+    
+               return node;
+           },
+    
+           getTooltipData: function () {
+               var me = this;
+    
+               return {
+                   component: me,
+                   tracking: me.getTrackingValue(),
+                   trackOver: me.getTrackOver(),
+                   value: me.getValue()
+               };
+           },
+    
+           /**
+            * Forcibly refreshes both glyph and tooltip rendering.
+            * @private
+            */
+           refresh: function () {
+               var me = this;
+    
+               if (me.invalidGlyphs) {
+                   me.refreshGlyphs(true);
+               }
+    
+               if (me.invalidTip) {
+                   me.refreshTip(true);
+               }
+           },
+    
+           /**
+            * Refreshes the glyph text rendering unless we are currently performing a
+            * bulk config change (initConfig or setConfig).
+            * @param {Boolean} now Pass `true` to force the refresh to happen now.
+            * @private
+            */
+           refreshGlyphs: function (now) {
+               var me = this,
+                   later = !now && (me.isConfiguring || me.isReconfiguring),
+                   el, glyphs, limit, on, off, trackerEl, valueEl;
+    
+               if (!later) {
+                   el = me.getGlyphTextNode(me.innerEl.dom);
+                   valueEl = me.getGlyphTextNode(me.valueEl.dom);
+                   trackerEl = me.getGlyphTextNode(me.trackerEl.dom);
+    
+                   glyphs = me.getGlyphs();
+                   limit = me.getLimit();
+    
+                   for (on = off = ''; limit--; ) {
+                       off += glyphs[0];
+                       on += glyphs[1];
+                   }
+    
+                   el.nodeValue = off;
+                   valueEl.nodeValue = on;
+                   trackerEl.nodeValue = on;
+               }
+    
+               me.invalidGlyphs = later;
+           },
+    
+           /**
+            * Refreshes the tooltip text rendering unless we are currently performing a
+            * bulk config change (initConfig or setConfig).
+            * @param {Boolean} now Pass `true` to force the refresh to happen now.
+            * @private
+            */
+           refreshTip: function (now) {
+               var me = this,
+                   later = !now && (me.isConfiguring || me.isReconfiguring),
+                   data, text, tooltip;
+    
+               if (!later) {
+                   tooltip = me.getTip();
+    
+                   if (tooltip) {
+                       data = me.getTooltipData();
+                       text = tooltip(data);
+                       me.setTooltipText(text);
+                   }
+               }
+    
+               me.invalidTip = later;
+           },
+    
+           /**
+            * Convert the coordinates of the given `Event` into a rating value.
+            * @param {Ext.event.Event} event The event.
+            * @return {Number} The rating based on the given event coordinates.
+            * @private
+            */
+           valueFromEvent: function (event) {
+               var me = this,
+                   el = me.innerEl,
+                   ex = event.getX(),
+                   rounding = me.getRounding(),
+                   cx = el.getX(),
+                   x = ex - cx,
+                   w = el.getWidth(),
+                   limit = me.getLimit(),
+                   v;
+    
+               if (me.getInherited().rtl) {
+                   x = w - x;
+               }
+    
+               v = x / w * limit;
+    
+               // We have to round up here so that the area we are over is considered 
+               // the value. 
+               v = Math.ceil(v / rounding) * rounding;
+    
+               return v;
+           },
+    
+           /**
+            * Convert the given rating into a width percentage.
+            * @param {Number} value The rating value to convert.
+            * @return {String} The width percentage to represent the given value.
+            * @private
+            */
+           valueToPercent: function (value) {
+               value = (value / this.getLimit()) * 100;
+               return value + '%';
+           }
+       }
+   });