Edit in JSFiddle

/**
 * @class Ext.ux.column.Sparkline
 * @author Tim Vasil <[email protected]>
 * 
 * Column to render sparklines via jQuery's Sparkline library (http://omnipotent.net/jquery.sparkline).
 *
 * For the best experience with this extension:
 * 
 * 1) See the "manualUpdate" property for information on preventing flicker during dynamic updates.
 * 2) Add a CSS style to ensure the tooltip is sized correctly by overriding incompatible Ext JS sizing:
 *    <pre>
 *    .jqstooltip {
 *       box-sizing: content-box;
 *       height: auto !important;
 *       width: auto !important;
 *    }
 *    </pre> 
 * 
 * MIT open source license
 */
Ext.define('Ext.ux.column.Sparkline', {
    extend: 'Ext.grid.column.Column',
    alias: ['widget.sparklinecolumn'],

    /**
     * {@cfg} {Object/Object[]} sparklineConfig 
     * 
     * Sparkline configuration object, as defined by the jQuery Sparkline API to be used across
     * all rows in this column.  This property is optional.
     * 
     * The config object is passed into the $.sparkline method to draw the sparkline.  If you specify an array of objects,
     * they're treated as composite configs and the charts are laid out on top of each other; you
     * can use this to create a multi-series sparkline.  As noted in the Sparkline documentation,
     * you may wish to set common min/max values across charts.  As a convenience, this implementation 
     * will automatically set the "composite" property to true for you on the subsequent configs in the array.
     * 
     * The special "dataIndex" property of the sparklineConfig object indicates the property
     * of the record to obtain the array of values for the chart.  If not specified, the dataIndex property
     * of the column is sued.
     * 
     * If you'd prefer to configure each cell's sparkline differently, leave sparklineConfig
     * undefined and set dataIndex to the property name in your model with values in this format:
     * <pre>
     * {
     *     config: { sparklineConfig },
     *     value: [ array of values for the chart ]
     * }
     * </pre> 
     */

    /**
     * {@cfg} {Boolean} minDataIndex
     * 
     * Optional data index to fetch the min value to be used across all charts (the chartRangeMin property).
     * 
     * Indicates whether the "min" should be set identically across all charts in the column.  When true, the min is 
     * determined dynamically by the smallest value of this property in the store.
     */
    
    /**
     * {@cfg} {Boolean} maxDataIndex
     * 
     * Optional data index to fetch the max value to be used across all charts (the chartRangeMax property).
     * 
     * Indicates whether the "max" should be set identically across all charts in the column.  When true, the max is 
     * determined dynamically by the largest value of this property in the store.
     */
    
    /**
     * Set to true to prevent flicker when Ext JS clears out cell contents during rendering updates.
     * This leaves the old sparkline in place until the async rendering of this class has a chance to overwrite it.
     * 
     * For this setting to work, you must apply a patch to Ext JS as follows:
     * <pre>
     *     Ext.override(Ext.grid.View, {
     *         shouldUpdateCell: function(column, changedFieldNames) {
     *             if (column.manualUpdate) {
     *                 return false;
     *             }
     *             return this.callParent(arguments);
     *         }
     *     });
     * </pre>
     * 
     * As an alternative, you can set "dataIndex" to a property that doesn't change across updates.
     * The drawback to this approach is you can no longer rely on native grid sorting.
     */
    manualUpdate: true,
    
    /**
     * Specifies the number of cells to render before relinquishing control back to the browser to process
     * user activity and other events.  Note that this setting applies to cells, not individual sparklines,
     * so if you have 5 composite sparklines in a cell that counts as just 1 rendering in the limit
     * specified here. 
     */
    asyncRenderMaxCells: 10,
    
    /** @Override */
    initComponent: function() {
        var me = this;

        if (me.sparklineConfig && !Ext.isArray(me.sparklineConfig)) {
            me.sparklineConfig = [me.sparklineConfig];
        }

        // IDs of cells whose charts need rendering
        me.syncQueue = [];

        // Map of IDs to sparkline values (for charts that need rendering)
        me.syncQueueData = {};

        me.callParent(arguments);
    },

    /** @Override */
    onAdded: function(container) {
        var me = this;

        // Hook column resize events so we can redraw sparklines whose widths are a percentage of the column width
        container.on('columnresize', function() {
            if (me.sparklineConfig && !/.*%$/.test(me.sparklineConfig.width)) {
                // Sparkline width is not a percentage -- no redraw needed
                return;
            }

            var grid = me.up('grid');
            var view = grid.getView();
            var store = grid.getStore();
            store.each(function(record) {
                // Redraw chart in this cell
                view.onUpdate(store, record, null, [me.dataIndex]);
            });
        });

        me.callParent(arguments);
    },

    /** @Override */
    defaultRenderer: function(value, metaData, record, rowIdx, colIdx, store, view) {
        var me = this;
        var config = me.sparklineConfig;
        var data;

        if (me.sparklineConfig) {
            data = [];
            for ( var i = 0; i < config.length; i++) {
                data.push({
                    config: config[i],
                    value: (config[i].dataIndex) ? record.get(config[i].dataIndex) : value
                });
            }
        } else {
            data = value;
        }

        // The only rendering we do synchronously is attaching an ID to the cell (column ID + record ID)
        var id = me.getId() + '-' + record.getId();
        metaData.tdAttr = 'id="' + id + '"';

        if (data) {
            // Asynchronously draw the sparkline.  We can't do it synchronously because: 
            // 1) this render method's HTML is buffered, not appended to the DOM immediately, and 
            // 2) it's not performance for more than 10 rows (especially in IE)
            if (!me.syncQueueData[id]) {
                me.syncQueue.push(id);
            }
            me.syncQueueData[id] = data;
            me.setSyncTimer();
        }
        return '';
    },

    /** @Override */
    destroy: function() {
        clearTimeout(me.syncTimer);
        this.callParent(arguments);
    },

    /**
     * @private
     * 
     * Ensures a timer is set to render all pending sparklines as soon as this thread completes its work.
     */
    setSyncTimer: function() {
        var me = this;
        if (!me.syncTimer) {
            me.syncTimer = setTimeout(function() {
                me.renderFromQueue();
            }, 0);
        }
    },

    /**
     * @private
     * 
     * Renders sparklines that need to be (re)drawn based on what's in the queue. To avoid noticeable lag, only a subset of
     * the queued items (which are processed in FIFO order) may be processed. If not all items are rendered, setSyncTimer is
     * called to process the remaining items as soon as this thread completes its work. (This allows the browser to process
     * user-initiated UI events before we resume rendering sparklines.)
     */
    renderFromQueue: function() {
        var me = this;
        var i = 0;
        delete me.syncTimer;

        var min = (me.minDataIndex) ? me.up('grid').getStore().max(me.minDataIndex) : undefined;
        var max = (me.maxDataIndex) ? me.up('grid').getStore().max(me.maxDataIndex) : undefined;

        // Limit the amount of rendering at one time to ensure the browser (especially IE) remains responsive
        for (i = 0; i < me.asyncRenderMaxCells && i < me.syncQueue.length; i++) {
            var id = me.syncQueue[i];
            var data = me.syncQueueData[id];
            var el = $('#' + id);
            delete me.syncQueueData[id];

            if (!el[0]) {
                // DOM element no longer exists -- nothing to do
                return;
            }

            if (!Ext.isArray(data)) {
                data = [data];
            }

            for ( var j = 0; j < data.length; j++) {
                var config = data[j].config;
                var value = data[j].value;

                if (min !== undefined) {
                    config.chartRangeMin = min;
                }
                if (max !== undefined) {
                    config.chartRangeMax = max;
                }
                if (j > 0) {
                    config.composite = true;
                }
                el.sparkline(value, config);
            }
        }

        // Remove the work we've done from the queue 
        me.syncQueue.splice(0, i);
        
        // Set the timer again if there's more work left to do
        if (me.syncQueue.length > 0) {
            me.setSyncTimer();
        }
    }
});


function addDataSamples(store) {
    store.each(function (item) {
        var cpu = addDataSample(item.get('cpuHistory'), 0, 100, 20);
        addDataSample(item.get('cpuKernelHistory'), 0, cpu * 0.9, 20);
        var memory = addDataSample(item.get('memoryHistory'), 0, 1024 * 1024 * 1024 * 32, 1024 * 1024 * 32);

        item.set('cpu', cpu);
        item.set('memory', memory);
        item.set('lastUpdate', new Date());
    });
}

function addDataSample(history, min, max, maxInc) {
    var value = Math.min(max, Math.max(min, (history[history.length - 1] || 0) + Ext.Number.randomInt(-maxInc, maxInc)));
    while (history.length >= 10) {
        history.shift();
    }
    history.push(value);
    return value;
}

Ext.override(Ext.grid.View, {
    shouldUpdateCell: function (column, changedFieldNames) {
        if (column.manualUpdate) {
            return false;
        }
        return this.callParent(arguments);
    }
});

Ext.onReady(function () {

    var customData = [10, -2, 30, 10, 100, 40, 6, 78, 23];
    var customLines = [{
        config: {
            type: 'bar'
        },
        value: [10, -2, 30, 10, 100, 40, 6, 78, 23]
    }, {
        config: {
            type: 'bar'
        },
        value: ['0:2', '2:4', '4:2', '4:1']
    }, [{
        config: {
            type: 'bar'
        },
        value: [10, -2, 30, 10, 100, 40, 6, 78, 23]
    }, {
        config: {
            type: 'line'
        },
        value: [-2, 30, 10, 100, 40, 6, 78, 23, 4]
    }], {
        config: {
            type: 'line'
        },
        value: [10, -2, 30, 10, 100, 40, 6, 78, 23]
    }, {
        config: {
            type: 'box',
            maxSpotColor: 'red'
        },
        value: [10, -2, 30, 10, 100, 40, 6, 78, 23]
    }, {
        config: {
            type: 'tristate'
        },
        value: [10, -2, 30, 10, 100, 40, 6, 78, 23]
    }, {
        config: {
            type: 'discrete'
        },
        value: [10, -2, 30, 10, 100, 40, 6, 78, 23]
    }, {
        config: {
            type: 'pie'
        },
        value: [10, -2, 4]
    }, {
        config: {
            type: 'bullet'
        },
        value: [10, 12, 12, 9, 7]
    }];

    Ext.define('Host', {
        extend: 'Ext.data.Model',
        idProperty: 'id',
        fields: ['id', 'name', 'cpu', 'cpuHistory', 'cpuKernelHistory', 'memory', 'memoryHistory', 'custom', 'lastUpdate']
    });

    var store = Ext.create('Ext.data.Store', {
        model: 'Host'
    });

    for (var i = 1; i <= 10; i++) {
        store.add({
            id: i,
            name: 'Process ' + i,
            cpuHistory: [],
            cpuKernelHistory: [],
            memoryHistory: [],
            custom: customLines[i - 1]
        });
    }
    for (i = 0; i < 10; i++) {
        addDataSamples(store);
    }

    var grid = Ext.create('Ext.grid.Panel', {
        renderTo: 'autoUpdateGrid',
        width: 600,
        height: 250,
        store: store,

        viewConfig: {
            markDirty: false
        },

        columns: [{
            text: 'Name',
            dataIndex: 'name'
        }, {
            text: 'CPU',
            dataIndex: 'cpu',
            align: 'right',
            width: 50,
            renderer: function (value) {
                return Ext.util.Format.number(value, '0%');
            }
        }, {
            text: 'CPU history',
            xtype: 'sparklinecolumn',
            dataIndex: 'cpuHistory',
            width: 110,

            sparklineConfig: [{
                type: 'line',
                dataIndex: 'cpuHistory',
                height: '19',
                width: '100%',
                chartRangeMin: 0,
                chartRangeMax: 1,
                spotColor: '',
                maxSpotColor: '',
                minSpotColor: '',
                highlightLineColor: '#000',
                highlightSpotColor: '#000',
                lineColor: '#080',
                fillColor: '#cec',
                tooltipFormatter: function (sparkline, options, fields) {
                    return Ext.util.Format.number(fields.y, '0%') + ' total<br />';
                }
            }, {
                type: 'line',
                dataIndex: 'cpuKernelHistory',
                chartRangeMin: 0,
                chartRangeMax: 1,
                spotColor: '',
                maxSpotColor: '',
                minSpotColor: '',
                highlightLineColor: '#000',
                highlightSpotColor: '#000',
                lineColor: '#800',
                fillColor: '#ecc',
                tooltipFormatter: function (sparkline, options, fields) {
                    return Ext.util.Format.number(fields.y, '0%') + ' kernel';
                }
            }]
        }, {
            text: 'Memory',
            dataIndex: 'memory',
            align: 'right',
            width: 100,
            renderer: function (value) {
                return Ext.util.Format.fileSize(value);
            }
        }, {
            text: 'Memory history',
            xtype: 'sparklinecolumn',
            dataIndex: 'memory',
            width: 110,

            maxDataIndex: 'memory',

            sparklineConfig: {
                type: 'line',
                dataIndex: 'memoryHistory',
                height: '19',
                width: '100%',
                spotColor: '',
                maxSpotColor: '',
                minSpotColor: '',
                highlightLineColor: '#000',
                highlightSpotColor: '#000',
                lineColor: '#008',
                fillColor: '#cce',
                tooltipFormatter: function (sparkline, options, fields) {
                    return Ext.util.Format.fileSize(fields.y);
                }
            }
        }, {
            text: 'Custom',
            xtype: 'sparklinecolumn',
            dataIndex: 'custom',
            width: 100,
            tdCls: 'custom-col'
        }]
    });

    setInterval(function () {
        addDataSamples(store);
    }, 1000);
});
<div id="autoUpdateGrid"></div>
.jqstooltip {
    box-sizing: content-box;
    height: auto !important;
    width: auto !important;
}
.custom-col {
    padding-left: 20px;
}

External resources loaded into this fiddle: