At Freelock we're in the midst of building dashboards for ourselves and for customers, to really dial in our process and let us know where to focus our improvements. Nothing beats having a visual representation of the data you're trying to analyze, and being able to change the parameters, update date ranges, drill down into details, and more.
Actually implementing a dashboard when you have data coming from multiple sources is a fairly large challenge in itself. But once you have that data available in a place you can reach from your browser, we find the Dojo Toolkit's Charts module very useful for providing that visualization. And as a Drupal shop, we build the actual dashboard in Drupal.
I'm finding I'm treading new ground here, creating a richer browser-based application with a Drupal back end. The key difference between what I'm doing with Dojo and the more typical approach with Drupal is that the blocks and charts in the browser can all feed off the same data source, without a page load.
This is the first in a series of posts outlining the best approaches I'm discovering as we go. In this one, I'm going to focus on the overall layout of where I put the HTML, the Javascript, and the data sources.
The challenge
First of all, let me provide some more detail of what we're trying to accomplish. I've set up a "Project Summary" dashboard tab, and on it we have a stacked, clustered column chart showing the amount of time spent and approved, per person on all projects with work during the reported week. Below that is a pie chart showing the same data, but aggregated for the entire team. And finally after that are individual pie charts for each team member showing their hours per project.
Two date selector widgets let you pick a date range for reporting, and load the data for that date range. All these charts then immediately update when the data is loaded.
Here's an example of the per-user pie charts:
Dojo Strengths
There's a bunch of Dojo techniques/modules I used to put together this dashboard, but the total amount of my code is pretty slim overall.
First of all, the page currently has 16 individual graphs, all loading data from a single data source sent in JSON to the browser -- and if you set date ranges in the distant past when we had different team members, more individual pies get added for them as well. The data sent to the browser is relatively slim and light, and not repeated 16 times. This is done by loading the data into a dojo/store object, and each chart loads a data series from this store with a specific query (e.g. user: "john", type: "approved"), all done browser side.
Using dojo/store/Observable, when we load a new set of data for a new time period, all the charts automatically update themselves with the new data value with pretty much no effort on our part.
Using dojo/topic, we can publish changes to filters, and use those to update multiple stores of data, which then updates all of their attached charts.
Using dojo/declare, dijit/_WidgetBase, and dijit/_TemplatedMixin, we can create our own custom "widget" that we can easily re-use. In the screenshot, we're showing a custom widget we're calling a "UserPie". When the data is updated, we can loop through all the users with data, find the maximum total hours, and set that on each UserPie. Our widget then automatically set its own radius based on the total hours for the data it has loaded as a result of its query.
In addition, you get a lot of niceties out of Dojo. With a simple callback function, each pie slice can show information in a tooltip, and this tooltip can be triggered on hover, click, or touch. With dojo/has, you can load the touch events on devices that support it, and skip loading them for everything else. The legends link to their charts, so mousing over (or tapping) an item in the legend highlights the corresponding data on the chart. And most of this has some level of WCAG accessibility standards baked in. (At least before I get in there...)
Hooking up Dojo widgets in Drupal
Now let's get to the meat -- how to build this into Drupal. The first thing, of course, is to install and configure the Dojo Toolkit module -- see my earlier article for more instructions. And create a custom Drupal module to contain your code.
The overall approach I've taken to getting these elements onto a dashboard page:
- Put as much of the Javascript as I can into Dojo AMD modules in separate files in a js/ subdirectory of the module.
- Implement hook_dojoconfig to register the name of my "package" in dojo.
- Define a Drupal block for each of my main sections -- project summary pie chart, project detail column chart, the date filtering widgets, and user pies
- Create a JSON data feed that responds appropriately to the filters at a particular path/endpoint (you can leverage the dojo_toolkit_views module in the dojo_toolkit-7.x-dev release, or provide your own callback).
- Use panels to drop the blocks where I want them on a dashboard page.
For an experienced Drupal user, most of these steps should be pretty straightforward -- but where things get interesting is #1, working with AMD.
Asynchronous Module Definition (AMD)
AMD basically allows you to load Javascript on demand, only once over the course of an entire page load. There's different flavors of it, and most Javascript toolkits are adopting some form of AMD. The earliest implementations came from RequireJS and Dojo.
With AMD, you require() the resources you need, and once they are loaded and available, your callback function is called.
Resources you require are defined in individual JS files using a define() function which may in turn require other resources. define() returns an object. In Dojo, using require, define, and declare, you can use patterns not ordinarily the way you would think of doing in Javascript. declare() returns what is essentially a class you can construct into an object. So typically the object you get back from a define is often either a singleton object (to avoid polluting the global namespace) or a class constructor (so you can create a new instance of a widget).
In practice I usually do attach objects to the global namespace, mainly so I can more easily debug them on the Chrome or Firebug console.
One other big thing to note here: DO NOT USE DRUPAL'S JS AGGREGATION ON AMD FILES! Dojo does have a build system you can use to aggregate all the AMD modules you use into a single (or small number) of JS layer files -- this build approach preserves the AMD loading functionality. So if you're using AMD, do not list the JS files in your module.info file or use drupal_add_js to load them.
Dashboard JS files
Ok. We've registered our module's namespace in hook_dojoconfig. How do we organize the actual JS code?
What I'm currently doing involves creating three different kinds of resources, which generally corresponds to Model, View, Controller.
- Model: reportStore.js -- this AMD module loads up the required data objects, and manages new data. Using dojo/topic, it subscribes to changes of filters, and on a change that triggers getting new data, does an Ajax call to load it. When the data comes back from the server, it updates the browser data store, while updating some totals we want for easy access. It returns a dojo/store object which contains the actual data loaded from the Ajax callback.
- View: userPie.js -- this is a custom widget we created for the individual user pie charts, and has an HTML template attached. This widget does contain logic for updating its radius based on the max hours and hours for this user, as well as keeping the legend and the pie chart in sync. It returns a class constructor.
- Controller: projectCharts.js, userCharts.js -- these files contain functions to load up and instantiate each chart. Each module require()'s the relevant model(s) and view(s) so that by the time your controller function is loaded it has everything it needs to display the graph. Each file returns an object with functions attached as members. The Drupal blocks then call the relevant function to display the graph.
A cool thing about render arrays in Drupal blocks
So now we come to the detail about how to hook this up to Drupal, and Drupal 7's renderable arrays have a pretty slick trick embedded into them I haven't seen documented anywhere but in code: the '#attached' array.
Basically we need the block to do three things:
- Provide a DOM element for the script to load with a chart.
- Require() the appropriate controller file.
- Call the controller function that does the rest of the work.
So here's a sample block defined in a hook_block_view:
case 'user_detail':
return array(
'subject' => t('User Detail'),
'content' => array(
'#type' => 'markup',
'#markup' => '<div id="user_detail_section"></div>',
'#attached' => array(
'dojo_toolkit_require' => array(
array('auriga_dashboard/userCharts','userCharts'),
),
'dojo_toolkit_addonload' => array(
array("
userCharts.initUsers('user_detail_section');
"),
),
),
),
);
As you can see, this is a pretty simple block. Using #markup, we drop a div with an id into the page where the node will appear. And here's the cool trick with #attached: it's capable of calling just about any function you want, not just drupal_add_js or drupal_add_css. Simply provide an array of functions to call as keys, and arguments inside a nested array.
You do need to nest your arguments inside two arrays to get them in the state your function might expect. But here we leverage two functions from the dojo_toolkit module to fulfill #2 and #3 of our requirements for the block:
dojo_toolkit_require() adds the specified AMD module to a require() at the top of the page and uses the second argument as a local JS variable that is available to any code passed into dojo_toolkit_addonload(). So in this case, when the page is loaded, the auriga_dashboard/userCharts.js file will get loaded. This file returns an object with a bunch of methods, and dojo_toolkit_require assigns it to "userCharts" in the require callback function. So then it's available in dojo_toolkit_addonload, where we can call the controller function, passing it the DOM id to replace.
The userCharts.js file then in turn requires() the reportStore and userPie modules and starts everything else up.
That's enough for now, let us know if you're finding this useful, or if you can use our services creating a dashboard for your company! In future posts I'll go into more detail about the model and loading data into the browser, widgets, and what the controller functions look like...
Add new comment