Fun with Highcharts, Part III

“It’s the little details that are vital. Little things make big things happen.”
John Wooden

So far, we have built a generic chart widget and a generic chart utility that produces chart objects from templates and dynamic data. Now it’s time to build the widget that will allow the user to pick and choose what they want to see, and then present the appropriate chart based on the user’s selections. Let’s start with a little HTML for the various pick lists that we will present to the user:

<link rel="stylesheet" type="text/css" href="/747e2219db213300f9699006db9619b9.cssdbx"/>
<link rel="stylesheet" type="text/css" href="/styles/retina_icons/retina_icons.css"/>
<div class="panel panel-default">
  <div class="panel-body form-horizontal">
    <div class="col-sm-12">
      <form id="form1" name="form1" novalidate>
        <div class="col-sm-3">
          <snh-form-field
            snh-model="c.data.group"
            snh-name="group"
            snh-label="Group"
            snh-type="choicelist"
            sn-value-field="value"
            sn-text-field="label"
            sn-items="c.data.config.groupOptions"
            ng-click="updateChart();"/>
        </div>
        <div class="col-sm-3">
          <snh-form-field
            snh-model="c.data.type"
            snh-name="type"
            snh-label="Task Type"
            snh-type="choicelist"
            sn-value-field="value"
            sn-text-field="label"
            sn-items="c.data.config.typeOptions"
            ng-click="updateChart();"/>
        </div>
        <div class="col-sm-3">
          <snh-form-field
            snh-model="c.data.frequency"
            snh-name="frequency"
            snh-label="Frequency"
            snh-type="choicelist"
            sn-value-field="value"
            sn-text-field="label"
            sn-items="c.data.config.freqOptions"
            ng-click="updateChart();"/>
        </div>
        <div class="col-sm-3">
          <snh-form-field
            snh-model="c.data.ending"
            snh-name="ending"
            snh-label="Period Ending"
            snh-type="choicelist"
            sn-value-field="value"
            sn-text-field="label"
            sn-items="c.data.config.endingOptions[c.data.frequency]"
            ng-click="updateChart();"/>
        </div>
      </form>
    </div>
    <div class="col-sm-12">
      <sp-widget widget="data.workloadWidget"></sp-widget>
    </div>
  </div>
</div>

This particular layout leverages our snh-form-field tag, but you could do basically the same thing with a simple sn-choice-list. There is nothing too exotic here, except maybe the lists of choices for the last element (Period Ending), which are contained in an object keyed by the value of the previous selection (Frequency). When you change the Frequency, the list of period ending dates changes to a list that is appropriate for the selected Frequency. Other than that one little oddity, it’s pretty vanilla stuff.

There are several reasons that I chose choicelist, which implements the sn-choice-list tag, for the snh-type of each pick list rather than reference, which implements the sn-record-picker tag. It would have been relatively easy to set up a filter on the sys_user_group table to create a record picker of active user groups, but I wanted to limit the choices to just those groups who had tasks assigned in the task table. That requires a GlideAggregate rather than a GlideRecord query, and I’m not sure how you would set that up in an sn-record-picker. For a choice list, you run the query yourself and then just set the value of the specified variable to an array created from your query results. For this widget, I created a config object to hold all of the choice list arrays, and used the following code to populate the array of choices for the group pick list:

cfg.max = 0;
cfg.maxGroup = '';
cfg.groupOptions = [];
var group = new GlideAggregate('task');
group.addAggregate('COUNT');
group.groupBy('assignment_group');
group.ordderBy('assignment_group');
group.query();
while (group.next()) {
	if (group.getDisplayValue('assignment_group')) {
		cfg.groupOptions.push({label: group.getDisplayValue('assignment_group'), value: group.getValue('assignment_group'), size: group.getAggregate('COUNT')});
		if (group.getAggregate('COUNT') > cfg.max) {
			cfg.max = group.getAggregate('COUNT');
			cfg.maxGroup = group.getValue('assignment_group');
		}
	}
}

For the choice list of task types, I wanted to wait until a group was selected, and then limit the choices to only those types that had been assigned to the selected group. This was another GlideAggregate, and that turned out to be very similar code:

var max = 0;
var defaultType = '';
data.config.typeOptions = [];
var type = new GlideAggregate('task');
type.addQuery('assignment_group', data.group);
type.addAggregate('COUNT');
type.groupBy('sys_class_name');
type.ordderBy('sys_class_name');
type.query();
while (type.next()) {
	if (type.getDisplayValue('sys_class_name')) {
		data.config.typeOptions.push({label: type.getDisplayValue('sys_class_name'), value: type.getValue('sys_class_name'), size: type.getAggregate('COUNT')});
		if (type.getAggregate('COUNT') > max) {
			max = type.getAggregate('COUNT') * 1;
			defaultType = type.getValue('sys_class_name');
		}
	}
}

The frequency choices, on the other hand, were just a hard-coded list that I came up with on my own. I wanted to be able to display the chart on a daily, weekly, monthly, quarterly, or yearly basis, so that’s the list of choices that I put together:

cfg.freqOptions = [
	{value: 'd', label: 'Daily', size: 7},
	{value: 'w', label: 'Weekly', size: 12},
	{value: 'm', label: 'Monthly', size: 12},
	{value: 'q', label: 'Quarterly', size: 8},
	{value: 'y', label: 'Yearly', size: 5}
];

The choices for period ending dates took a bit more code. For one thing, I needed a different list of choices for each frequency. For another, the methodology for determining the next date in the series was slightly different for each frequency. That meant that much of the code was not reusable, as it was unique to each use case. There is probably a way to clean this up a bit, but this is what I have working for now:

cfg.endingOptions = {d: [], w: [], m: [], q: [], y: []};
var todaysDateInfo = getDateValues(new Date());
var today = new Date(todaysDateInfo.label);
var nextSaturday = new Date(today.getTime());
nextSaturday.setDate(nextSaturday.getDate() + (6 - nextSaturday.getDay()));
dt = new Date(nextSaturday.getTime());
for (var i=0; i<52; i++) {
	cfg.endingOptions['d'].push(getDateValues(dt));
	cfg.endingOptions['w'].push(getDateValues(dt));
	dt.setDate(dt.getDate() - 7);
}
dt = new Date(today.getTime());
dt.setMonth(11);
dt = getLastDayOfMonth(dt);
cfg.endingOptions['y'].push(getDateValues(dt));
dt = new Date(today.getTime());
dt.setDate(1);
dt.setMonth([2,2,2,5,5,5,8,8,8,11,11,11][dt.getMonth()]);
dt = getLastDayOfMonth(dt);
cfg.endingOptions['q'].push(getDateValues(dt));
dt = new Date(today.getTime());
for (var i=0; i<36; i++) {
	dt = getLastDayOfMonth(dt);
	var mm = dt.getMonth();
	cfg.endingOptions['m'].push(getDateValues(dt));
	if (mm == 2 || mm == 5 || mm == 8 || mm == 11) {
		cfg.endingOptions['q'].push(getDateValues(dt));
	}
	if (mm == 11 && i != 0) {
		cfg.endingOptions['y'].push(getDateValues(dt));
	}
	dt.setDate(1);
	dt.setMonth(dt.getMonth() - 1);
}

That takes care of the four choice lists and the code to come up with the values for the four choice lists. We’ll want something selected when the page first loads, though, so we’ll need some additional code to come up with the initial values for each of the four selections. For the group, my thought was to start out with the group that had the most tasks, and if the user was a member of any groups, group of which the user was a member with the most tasks would be event better. Here’s what I came up with the handle that:

function getDefaultGroup() {
	var defaultGroup = '';

	var max = 0;
	var group = new GlideAggregate('task');
	if (data.usersGroups.size() > 0) {
		var usersGroups = '';
		var separator = '';
		for (var i=0; i<data.usersGroups.size(); i++) {
			usersGroups = separator + "'" + data.usersGroups.get(i) + "'";
			separator = ',';
		}
		group.addQuery('assignment_group', 'IN', usersGroups);
	}
	group.addAggregate('COUNT');
	group.groupBy('assignment_group');
	group.ordderBy('assignment_group');
	group.query();
	while (group.next()) {
		if (group.getDisplayValue('assignment_group')) {
			if (group.getAggregate('COUNT') > max) {
				max = group.getAggregate('COUNT') * 1;
				defaultGroup = group.getValue('sys_class_name');
			}
		}
	}
	if (!defaultGroup) {
		defaultGroup = data.config.maxGroup;
	}

	return defaultGroup;
}

Since the type choices are dependent on the group selected, that code is already built into the type selection list creation process (above). For the initial frequency, I just arbitrarily decided to start out with daily, and for the initial period ending date, I decided that the current period would be the best place to start as well. That code turned out to be pretty basic.

data.frequency = 'd';
data.ending = data.config.endingOptions[data.frequency][0].value;

With the initial choices made, we now need to work out the process of gathering up the data for the chart based on the choice list selections. That’s a bit of a task as well, so let’s make that our focus the next time out.