sencha-logoIn our last installment, we dealt with a very common occurrence that happens in the course of real-life development: putting out the fire of a managerial ad-hoc requirement. In our case, we implemented a login and role-management system (something which was not required in initial requirements-gathering). And even though this came out of the blue, we saw that the MVC structure and event-based development methodology of ExtJS allowed us to quickly and easily respond to the crisis. Phew.

But now, we’re back on track, and ready to develop some reports!

Reports are interesting things in the domain of the business application. In a lot of ways, reports are the ultimate goal. Sure, we need an inventory management system, and yes, being able to manage content for our Cars is really nice. However, from the perspective of the people writing our paychecks, the reports are where the real value comes into play. We can make the rest of the application as awesome as we like, but they’ll probably never use it or notice it. But if we develop some really nice reports that gives them instant visibility into what they care about…well, we’ve not only given them what they ultimately care about, but have probably also helped our case when annual pay raise approvals come around.

NOTE: The code for this session can be found on GitHub.

Reports Vs. The Grid

On some level, we’ve already created some report-esque structures. After all, you could *technically* export our Inventory grid, do some Excel-foo, and get at any numbers that you could want. Of course, this doesn’t work, for a couple reasons.

First, the people who care about the details of the Inventory management system are generally not the people who care about the reports about the inventory.  While there is certainly overlap, the Sales Manager cares about some very specific, high-level details; a data-dump of the Inventory is not going to help her justify the new hiring request.

Second, there are many times in which raw data isn’t meaningful in a reporting capacity. Most robust reporting engines take raw data and do various transformations to the data in order to get at something intelligible and reportable: for example, taking all the cars sold in a fiscal year, aggregating the sales by make/model, and reporting the totals. Ultimately, the most useful reports aren’t interested in data points; they are interested in the data en toto, and how this aggregated collection can be meaningfully broken down into various categories.

Third, imagine that you have hundreds of thousands of data points that need to exist in a single report. If we were to try to apply all of these to a chart, we’d not only have to wait FOREVER for the data to render, but we’d have such a large number of data points that it would be virtually impossible to make meaning from it.

The practical implication of these considerations is that in order to create reporting facilities within our application that are helpful, we’ll need to approach the data in a bit different way than we have before. But this is good, because we’ll get to use some shiny charts in the process!

ExtJS Charts

ExtJS 4 has some pretty amazing charting capabilities. If you’ve never done so, take a few minutes (or hours!) browsing the various chart examples in the documentation. Not only are they beautiful and animated, but like other components within ExtJS, can be wired into model-driven stores.

Before we dive into creating our first chart, though, there is a pretty important principle that we need to be clear about. While ExtJS 4 charts are super-awesome at displaying data in visually-stunning manner, they are NOT good at transforming your data.

Why do I mean by this?

Let’s suppose that we want to create a pie chart that will break-down the “sold” cars in our inventory by Make and Model. In order to pull this off, our data will need to reflect the aggregated sales by Make and Model. So for our first attempt, we might be tempted just to chuck our entire inventory store at our chart, hoping that it will automatically slice and dice our data into a nicely aggregated pie chart.

Unfortunately, it’s simply not going to do this for us. Yes, it will render whatever data we configure; but in terms of the fundamental transformation required to display our Inventory data in the form that we need for the pie chart, we’ll not have success.

After your first run-in with this apparent “limitation”, you might be a bit disappointed. But before you go down this road, I’d encourage you to step back and think about what we discussed above. Remember, a report (of which our chart is going to be a part) is really about the “big picture”–we’re not interested in data dumps, or detailing every individual data point. Rather, the kind of report we want is ultimately the distillation of potentially thousands of data points (which as a bulk are only so useful) into a human-readable, visually discernible format. If we take this perspective from the start, we’ll not only understand why a different data set is needed, but also why this is ultimately the more beneficial way to produce the report that we’re after.

Let’s Get It On

Enough with the faux-report theory; let’s build a chart!

The Data

As always, let’s talk first about our data model. For our first chart, we want a report that will give us a breakdown of Total Sales by Make and Model. So let’s create a new data Model in model/report called Make.js:

/**
 * {@link Ext.data.Model} for Make Sales object
 */
Ext.define('CarTracker.model.report.Make', {
    extend: 'Ext.data.Model',
    fields: [
        // non-relational properties
        {
            name: 'Make',
            type: 'string',
            persist: false
        },
        {
            name: 'Model',
            type: 'string',
            persist: false
        },
        {
            name: 'TotalSales',
            type: 'int',
            persist: false
        }
    ]
});

A few things to note:

First, we’re not extending our usual CarTracker.model.Base. Since our report is read-only, we’ll just be returning ad-hoc values from the server, so this data model has nothing in common with the others we’ve used thus far.

Another thing to note about this data model is that since we’re only concerned with these 3 fields, it’s obvious that we’ll be dealing with aggregate data returned from the server. So then, each data point will represent an aggregated total of data that matches the make/model combination, rather than individual Inventory records.

Likewise, our store is a bit different:

Ext.define('CarTracker.store.report.Makes', {
    extend: 'CarTracker.store.Base',
    alias: 'store.report.make',
    requires: [
        'CarTracker.model.report.Make'
    ],
    restPath: '/api/reports/make',
    storeId: 'report_Makes',
    model: 'CarTracker.model.report.Make',
    remoteSort: false,
    remoteGroup: false,
    groupField: 'Make'
});

In this case, we’ve turned off remote sorting (since we’ll pull back the entire data set on load), and added the groupField config. This will allow us to do some cool stuff with the grid later on :).

The View

For this report, we’re going to do a combination of an Ext.grid.Panel and a Ext.chart.Chart. The grid will allow us to tabularly list and summarize the totals, while the chart (a pie-chart, in this example) will us to visually represent the tabular data.

While there are a number of ways to pull this off, a nice way to manage the dual-display of these components will be through a Panel with a Border layout. Not only will this help us layout the grid and chart, but will give us a nice option for resizing the panels via a splitter.

To do this, let’s create 3 new views, all under view/report/make:

  • Dashboard.js (border layout)
  • List.js (grid)
  • Chart.js (chart)

Ultimately, we want it to look something like this:

MakeReport

 

Grid.js

Our grid will be like all the rest, but with a few interesting additions. First, we’ll add a Grouping feature to group the results by Make. Second, we’ll add a Summary feature that will display the total Sales at the bottom of our grid.

NOTE: A Feature is really just an ExtJS Plugin that is specially designed for Ext.grid.Panels, providing a few additional hooks into the lifecycle of the grid that allow developers to really expand the functionality and display options for the grid.

Adding a Grouping is very simple. First, if you remember our store config, we specified a groupField configuration. This config will tell our grid that it should automatically group the store data by this property. To activate this grouping, we simply need to do the following in the grid config:

features: [
    {
        ftype: 'grouping'
    }
]

And in the same vein, adding a Summary row is really simple as well. To apply a summary row, need to add our feature as before (e.g., ftype: ‘summary’), and then configure our columns to support the summary. Here is the Total Sales column config as an example:

{
	text: 'Total Sales',
	dataIndex: 'TotalSales',
	renderer: function( value, metaData, record, rowIndex, colIndex, store, view ) {
		return Ext.util.Format.usMoney( value );
	},
	summaryType: 'sum',
	summaryRenderer: function( value, summaryData, dataIndex ) {
            return 'Grand Total: ' + Ext.util.Format.usMoney( value ) + ''; 
        },
	flex: 1
}

As you can see, the only thing we had to add were the two new configs: summaryType and summaryRenderer. The summaryType config tells our column what kind of summary is should perform, while the summaryRenderer functions in a similar capacity to the regular column renderer, allowing us to customize the format of the string that will ultimately be rendered. And of course, the Summary feature automatically takes care of rendering the summary row for us at the bottom of the grid. Awesome!

NOTE: Valid summaryType values include:

  • count
  • sum
  • min
  • max
  • average
  • custom function definition

Chart.js

Now we get to the interesting stuff! In view/report/make, our Chart.js component looks like so:

Ext.define('CarTracker.view.report.make.Chart', {
    extend: 'Ext.chart.Chart',
    alias: 'widget.report.make.chart',
    requires: [
        'Ext.chart.series.Pie',
        'Ext.data.JsonStore'
    ],
    store: Ext.create('Ext.data.JsonStore', {
        fields: [ 'Model', 'TotalSales' ]
    }),
    cls: 'x-panel-body-default',
    animate: true,
    border: false,
    shadow: true,
    legend: {
        position: 'right'
    },
    insetPadding: 60,
    theme: 'Base:gradients',
    series: [
        {
            type: 'pie',
            field: 'TotalSales',
            showInLegend: true,
            donut: 10,
            tips: {
                trackMouse: true,
                width: 170,
                height: 28,
                renderer: function( storeItem, item ) {
                    //calculate percentage.
                    var total = 0;
                    storeItem.store.each(function(rec) {
                        total += rec.get('TotalSales');
                    });
                    this.setTitle(storeItem.get( 'Model' ) + ': ' + Math.round( storeItem.get( 'TotalSales' ) / total * 100 ) + '% (' + Ext.util.Format.usMoney( storeItem.get( 'TotalSales' ) ) + ')' );
                }
            },
            highlight: {
                segment: {
                    margin: 20
                }
            },
            label: {
                field: 'Model',
                display: 'rotate',
                contrast: true,
                font: '18px Arial'
            }
        }
    ]
});

Charts, of course, have TONS of possible configuration options, so I’ll leave you to explore what’s possible. However, I will point out a few of the more interesting parts (highlighted in bold above).

First, notice that we are using an inline store. We could create an entirely new store class, however, in this example, we actually just want to use data that’s already loaded by our grid to populate our chart’s store (more on this later).

Next, as we want this particular chart to be a pie chart, we accomplish this by using the Ext.chart.series.Pie in the series config. As you can see, all we really have to do is define the type and field from our store, and the chart will take care of itself.

We’ve also souped this particular pie chart up a bit by specifying a custom tooltip renderer. So when we hover over each slice of the pie, we’ll display a custom tool tip that shows the percentage of the pie that the individual segment represents, based on a calculation of the total store’s data:

Percentage

Putting It All Together

Now that we’ve got our model, stores, and views knocked out, let’s wire it all together in a controller. In controller, let’s create a new class called Reports.js.

In this class, there are three main goals we want to accomplish:

  • Load data into the grid and chart
  • When we click on a particular model of car, re-draw the chart to show only the breakdown for the selected make
  • Allow the data to be reloaded and the chart to be re-drawn for the full results

First things first, let’s load the data. To do this, I’m simply attaching a listener to the beforerender event of Dashboard component, and calling the following method:

/**
 * Loads the component's store
 * @param {Ext.panel.Panel} panel
 * @param {Object} eOpts
 */
loadSalesByMake: function( panel, eOpts ) {
    var me = this,
        store = panel.down( 'grid' ).getStore(),
        chart = panel.down( 'chart' ),
        data=[];
    // add extra params
    store.getProxy().extraParams = {
        filter: Ext.encode([
            {
                property: 'SalesByMake',
                value: true
            },
            {
                property: 'IsSold',
                value: true
            }
        ])
    };
    // load the store
    store.load({
        callback: function( records, operation, success ) {
            // load full range of data
            chart.getStore().loadData( store.getRange() );
        }
    });
}

Nothing to it. The interesting bit is in bold. As you’ll remember, we defined our chart’s store as being an abstract, inline implementation of Ext.data.JsonStore. So then, in the callback method of our grid’s main store, we simply load our chart’s simple store with the data from the grid. Since our root store (Makes.js) is remote, this prevents us from having to run to the server twice to retrieve the same data, but still allow us to have two store instances that are not directly bound to each other.

Next, we want to handle being able to visually pare down the chart when we select a particular Model within our grid. That is, if we have 3 cars all with the “Dodge” Make, we want to the filter the chart’s store to show only results with the “Dodge” Make, while leaving our grid results in tact. To do  this, we can add an itemclick event handler to our grid, and call the following method:

/**
 * Filters chart store by selected Make
 * @param {Ext.view.View} view
 * @param {Ext.data.Model} record
 * @param {Object} item
 * @param {Number} index
 * @param {Object} e
 * @param {Object} eOpts
 */
loadSalesByMakeDetailChart: function( view, record, item, index, e, eOpts ) {
    var me = this,
        make = record.get( 'Make' ),
        data = [],
        chart = view.up( '[xtype=report.make.dashboard]' ).down( 'chart' ),
        store = chart.getStore();
    // clear filter
    store.clearFilter( true );
    // filter by make
    store.filter( 'Make', make );
}

Since we’ve already loaded our chart’s store with data, we can now simply filter the results by the selected Make, which will automatically trigger the  chart to redraw, like so:

FilteredChart

And finally, we simply need a way to reload our chart’s data. To do this, we can add a “Reload” button to our grid, who’s click handler will fire the following method:

/**
 * Reloads chart from full grid data
 */
loadSalesByMakeFullChart: function() {
    var me = this,
        view = me.getSalesByMakeDashboard(),
        grid = view.down( 'grid' ),
        store = grid.getStore(),
        chart = view.down( 'chart' ),
        chartStore = chart.getStore();
    // clear any filters
    chartStore.clearFilter( false );
    // load full range of data
    chartStore.loadData( store.getRange() );
}

Easy-peasy.

Wrapping Up With Some Homework

So there we have it. Charts are just like any other ExtJS component: they are bound to stores and can be easily configured and manipulated through configuration and events. Obviously, there is a lot more that can be done with charts, but for now we’ll wrap up and call this good.

However, you’re not quite done. To get some practice with another type of chart, I’d encourage you to try your hand at creating one additional report. For this one, we want a report that will show the Total Car Sales by Month. Obviously, since our Inventory data model doesn’t have the concept of a “Sale Month”, this data is something that will need to be aggregated server-side, and then returned to a custom data model in our application.

Try to emulate something like this:

BarChart

Good luck!