Dynamic Service Portal Breadcrumbs, Corrected

“Most good programmers do programming not because they expect to get paid or get adulation by the public, but because it is fun to program.”
Linus Torvalds

While I was playing around with my workload chart, I noticed a little bug in my dynamic breadcrumbs widget: returning to the Home page does not reset the breadcrumbs for a new trail out from the home page. Instead, all of the previous trail of pages remains intact. This is not the way that this is supposed to work. Once you return to the home page, the trail should start over. At first, I never noticed this, because I never added the breadcrumbs widget to the home page. Without the breadcrumbs widget on the home page, I wouldn’t expect it to reset. But once I did that and it still didn’t work, I had to get busy trying to figure out why that was.

The breadcrumbs data that was left behind on the previous page is retrieved from the User Preference on the server side, and then the new breadcrumbs data is established on the client side. Initially, the breadcrumbs data is set to an empty Array, and then if we are not on the home page, we push all of the existing pages onto the array until we come across the current page or run out of existing pages. In the case of the home page, the empty array is all that remains. Here is the relevant code:

c.breadcrumbs = [];
var thisPage = {url: $location.url(), id: $location.search()['id'], label: c.data.page || document.title};
if (thisPage.id != $rootScope.portal.homepage_dv) {
	var pageFound = false;
	for (var i=0;i<c.data.breadcrumbs.length && !pageFound; i++) {
		if (c.data.breadcrumbs[i].id == thisPage.id) {
			c.breadcrumbs.push(thisPage);
			pageFound = true;
		} else {
			c.breadcrumbs.push(c.data.breadcrumbs[i]);
		}
	}
	if (!pageFound) {
		c.breadcrumbs.push(thisPage);
	}
}
c.data.breadcrumbs = c.breadcrumbs;
c.server.update();

On the server side of things, my intent was to save the breadcrumbs once established, and here is the code that was supposed to handle that:

if (input.breadcrumbs) {
	gs.getUser().setPreference('snhbc', JSON.stringify(input.breadcrumbs));
}

My thinking was that, if the breadcrumbs were established on the client side, then save them to the User Preference. Unfortunately, the simple conditional input.breadcrumbs returns false for an empty array, not true as I had assumed (hey, an empty array is still something and not null!); therefore, the saving of the breadcrumbs was not executed when on the home page. I should have know that, I guess, but I’m no longer young enough to know everything. I still get to learn something new every day. Once I figured that out, I changed it to this:

if (Array.isArray(input.breadcrumbs)) {
	gs.getUser().setPreference('snhbc', JSON.stringify(input.breadcrumbs));
}

A simple change, but one that made it work the way that I had intended instead of the way that I had coded it. That took care of that little issue. I included the updated breadcrumbs widget in the last Update Set for my Highcharts example, but for those of you who are only interested in the breadcrumbs widget, I created a separate Update Set, which you can grab here.

Update: There is an even better version, which you can find here.

Fun with Highcharts, Part VII

“Time, which alone makes the reputation of men, ends by making their defects respectable.”
Voltaire

Once again, things were not quite as bad as I had originally envisioned. I definitely disliked returning to my workload chart page and having it revert back to all of the original, default settings. This was not good. But how best to fix it? I didn’t have that problem with my Data Table Content Selector, which also has multiple options, but that was built using a little bit different approach. Although there are two independent widgets on the page, they don’t really communicate with one another. The selector on the current page communicates with the Data Table widget on the next page, passing all of the information via URL parameters when it builds the URL for the next page. Reconfiguring the workload chart widget and underlying generic chart widget to adopt this philosophy seemed like a lot more work that I really wanted take on right at the moment.

Instead, I decided to leverage the same hack that I used in my dynamic breadcrumbs widget, leveraging the User Preferences feature to preserve the selections and then picking them back up each time the page loads. Here is the additional code I added to save the user’s selections in a User Preference that I called workload_selections:

var selections = {};
selections.group = data.group;
selections.type = data.type;
selections.frequency = data.frequency;
selections.ending = data.ending;
gs.getUser().setPreference('workload_selections', JSON.stringify(selections));

User Preferences are strings, so you have to convert the object to a string before you save it, and convert it back to an object when you fetch it back. Here is the code that I used to retrieve it once the page loads again:

selections = gs.getPreference('workload_selections');
if (selections) {
	selections = JSON.parse(selections);
	if (selections.group) {
		data.group = selections.group;
		data.type = getDefaultType();
		data.type = selections.type;
		data.frequency = selections.frequency;
		data.ending = selections.ending;
	}
}

You might have noticed that the value for the type selection was set twice, once to the default value and then again to the value saved in the User Preference. That’s because the getDefaultType function is actually a dual purpose function; not only does it return the default type value, but before it does so, it sets up the type options that are appropriate for the selected group (different groups are associated with different types of tasks). Since the group value preserved may be different than the default value, the appropriate list of type choices needs to be generated for that group before the preserved type value is established. Other than that, it’s pretty straightforward stuff.

That resolved the last known defect that I have been able to find so far, so it’s finally time to release another Update Set. If you happen to pull it down and try to use it, let me know if you find any others … thanks!

Update: There is a better (expanded) version here.

Fun with Highcharts, Part VI

“If you’re going through Hell, keep going.”
Someone other than Winston Churchill

Well, it turns out that it wasn’t as bad as I had originally imagined. I converted my server side GenericChartUtil Script Include into a client side UI Script, then created a Widget Dependency referencing the UI Script, and then finally associated the Widget Dependency to my Generic Chart widget. That pushed the chart object generation from the server side to client side (where I would no longer lose any functions in the chart object), but to make it all work, I needed to pass the chart data and chart type around rather than the completed chart object.

On my Generic Chart widget, I removed the chartObject option and replaced it with two new widget options, chartType and chartData. Then I added a line of code in the client side script to pass the chartData and chartType to the new client side UI Script to generate the chartObject. The client side code for the widget now looks like this:

function($scope, $rootScope, $location) {
	var c = this;
	if (c.data.chartData) {
		c.data.chartData.location = $location;
		$scope.chartOptions = genericChartUtil.getChartObject(c.data.chartData, c.data.chartType);
	}
	if (c.options.listen_for) {
		$rootScope.$on(c.options.listen_for, function (event, config) {
			if (config.chartData) {
				config.chartData.location = $location;
				$scope.chartOptions = genericChartUtil.getChartObject(config.chartData, config.chartType);
			}
		});
	}
}

On my Workload Chart widget, I removed all references to the deleted Script Include on the server side, and then modified the broadcast message on the client side to pass the chartData and chartType rather than the entire generated chartObject. That code now looks like this:

function($scope, $rootScope) {
	var c = this;
	$scope.updateChart = function() {
		c.server.update().then(function(response) {
			c.data.config = response.config;
			c.data.group = response.group;
			c.data.type = response.type;
			c.data.frequency = response.frequency;
			c.data.ending = response.ending;
			c.data.chartData = response.chartData;
			$rootScope.$broadcast('refresh-workload', {chartData: c.data.chartData, chartType: 'workload'});
		});
	}
}

That solved my earlier problem of losing the functions built into the chart objects that were generated on the server side. Now I could get back to what I was trying to do in the first place, which was to set things up so that you could click on any given data point on the chart and pull up a list of the records that were represented by that value. This code was now working as it should:

plotOptions: {
        series: {
            cursor: 'pointer',
            point: {
                events: {
                    click: function () {
                        alert('Category: ' + this.category + ', value: ' + this.y);
                    }
                }
            }
        }
    },

For my link URL to show the list of records represented by the chart item clicked, I was going to need the name of the table and the filter used in the GlideAggregate that calculated the value. Neither one of those was currently passed in the chart data, so the first thing that I needed to do was to modify the code that generated the chartData object to include those values. That code now looks like this:

function gatherChartData() {
	var task = new GlideAggregate(data.type);
	var periodData = getPeriodData();
	var chartData = {};
	chartData.table = data.type;
	chartData.filter = {Received: [], Completed: [], Backlog: []};
	var filter = '';
	chartData.title = task.getPlural() + ' assigned to ' + findOption(data.config.groupOptions, data.group).label;
	chartData.subtitle = periodData.frequencyInfo.label + ' through ' + periodData.endingDateInfo.label;
	chartData.labels = periodData.labels;
	chartData.received = [];
	chartData.completed = [];
	chartData.backlog = [];
	for (var i=1; i<periodData.endDate.length; i++) {
		// received
		filter = 'assignment_group=' + data.group + '^opened_at>' + periodData.endDate[i-1] + '^opened_at<=' + periodData.endDate[i];
		task.initialize();
		task.addAggregate('COUNT');
		task.addEncodedQuery(filter);
		task.query();
		task.next();
		chartData.received.push(task.getAggregate('COUNT') * 1);
		chartData.filter.Received.push(filter);
		// completed
		filter = 'assignment_group=' + data.group + '^closed_at>' + periodData.endDate[i-1] + '^closed_at<=' + periodData.endDate[i];
		task.initialize();
		task.addAggregate('COUNT');
		task.addEncodedQuery(filter);
		task.query();
		task.next();
		chartData.completed.push(task.getAggregate('COUNT') * 1);
		chartData.filter.Completed.push(filter);
		// backlog
		filter = 'assignment_group=' + data.group + '^opened_at<=' + periodData.endDate[i] + '^closed_at>' + periodData.endDate[i] + '^ORclosed_atISEMPTY';
		task.initialize();
		task.addAggregate('COUNT');
		task.addEncodedQuery(filter);
		task.query();
		task.next();
		chartData.backlog.push(task.getAggregate('COUNT') * 1);
		chartData.filter.Backlog.push(filter);
	}
	return chartData;
}

Since I need the filter value for multiple purposes, I first assigned that to a variable, and then used that variable wherever it was needed. Inside of Highcharts, my only reference to the series was the category name, which is why there is an odd capitalized property key for the chartData.filter properties (the category names are labels on the chart, so they are capitalized). These changes gave me the data to work with so that I could modify the onclick function to look like this:

plotOptions: {
	series: {
		cursor: 'pointer',
		point: {
			events: {
				click: function (evt) {
					var s = {id: 'snh_list', table: chartData.table, filter: chartData.filter[this.series.name][this.index]};
					var newURL = chartData.location.search(s);
					spAriaFocusManager.navigateToLink(newURL.url());
				}
			}
		}
	}
},

In order for that to work, I need to pass the $location object to Highcharts as well, so my client side GenericChart widget code ended up looking like this:

function($scope, $rootScope, $location) {
	var c = this;
	if (c.data.chartData) {
		c.data.chartData.location = $location;
		$scope.chartOptions = genericChartUtil.getChartObject(c.data.chartData, c.data.chartType);
	}
	if (c.options.listen_for) {
		$rootScope.$on(c.options.listen_for, function (event, config) {
			if (config.chartData) {
				config.chartData.location = $location;
				$scope.chartOptions = genericChartUtil.getChartObject(config.chartData, config.chartType);
			}
		});
	}
}

To get back to the chart itself, once you clicked on a data point to see the underlying records, I added my dynamic breadcrumbs widget to the the top of the chart page and to the top of a new list page that I created for this purpose. Now it was time to test things out …

Well, as often seems to be the case with these things, there is good news and bad news. The good news is that clicking on the data points on the chart actually does bring up the list of records, which is very cool. Even though I had to do a considerable amount of restructuring of my initial concept, everything now seems to work. And I like the feature. Clicking on a bar or point on the line now takes you to a list of the records that make up the value for that data point. And when you are done, you can click on the breadcrumb for the chart and get back to the chart itself. All of that works beautifully.

Unfortunately, when you get back to the chart page, it reverts to the original default settings for all of the options. If you used any of the four selections at the top of the page to get to a specific chart configuration, and then clicked on a data point to see the underlying records, when you returned to the chart, you were no longer looking at the chart that you had selected. You were back to the original, default values for all of the selections. The page basically restarts from the beginning. That, I do not like at all.

The primary reason for that behavior is that your chart option selections are not part of the URL, so they are not preserved when you click on the breadcrumb to return. Making them part of the URL would mean another complete reconfiguration of the way in which that chart works, but something is going to have to be done. I don’t like it the way that it works now. Originally, I was going to release a new Update Set with all of these changes, but I’m not happy with the way things are working right now. I’m going to have to do a little bit more work before I’m ready to release another version. Hopefully, I can do that next time out.

Fun with Highcharts, Part V

“It’s always something.”
Roseanne Roseannadanna

I decided to enhance my workload chart by adding the ability click on a bar or point on the backlog line and bring up a list of all of the tasks represented by that data on the chart. Highcharts makes that super simple to do, so I went out and grabbed some example code off of the Interwebs and added this to my workload chart template:

plotOptions: {
        series: {
            cursor: 'pointer',
            point: {
                events: {
                    click: function () {
                        alert('Category: ' + this.category + ', value: ' + this.y);
                    }
                }
            }
        }
    },

That was enough to prove that everything worked, after which I was going to modify the click function to navigate to a Data Table page with the appropriate filter to list out all of the tasks represented by the data point selected. Unfortunately, when I fired it up to test it out, nothing happened when I clicked on a bar or a point along the line. The cursor did change to a pointer, so that part was definitely working, but no alerts popped up no matter where I clicked. I hate it when that happens!

It took me a little while to figure this out, but after a little bit of digging around I finally reached the heart of the issue: I built the chart object on the server side, and when it was transferred to the client side where it was to be used, it was converted to a JSON string somewhere along the lines in the background Ajax process, and that conversion removed the function (JSON only preserves data; any functions are lost in translation). While it was nice to finally understand the root of the problem, the implication was that my whole way of going about this was pretty much invalidated. I can’t build a chart object on the server side and then pass it to the client side and retain any functions that might be a part of the object. The chart object will need be generated on the client side where it will be used. That means a total redesign of the entire concept.

Well, I guess that guy Charles Lauller knew what he was talking about. Time to start over with a blank sheet of paper

Fun with Highcharts, Part IV

“Lost wealth may be replaced by industry, lost knowledge by study, lost health by temperance or medicine, but lost time is gone forever.”
Samuel Smiles

Now that we have our generic chart widget, our generic chart object generator, and the choice lists and default selections for our example chart, we just need to come up with the code to gather up the data for the chart based on the selections. A number of elements are based on the period selected, so collecting that data would seem like an important first step. I organized all of that into a function that I called getPeriodData:

function getPeriodData() {
	var periodData = {};
	periodData.frequencyInfo = findOption(data.config.freqOptions, data.frequency);
	periodData.endingDateInfo = findOption(data.config.endingOptions[data.frequency], data.ending);
	periodData.labels = [];
	periodData.endDate = [];
	for (var i=0; i<=periodData.frequencyInfo.size; i++) {
		var dt = new Date(periodData.endingDateInfo.label);
		if (data.frequency == 'd') {
			dt.setDate(dt.getDate() - (periodData.frequencyInfo.size - i));
		} else if (data.frequency == 'w') {
			dt.setDate(dt.getDate() - (periodData.frequencyInfo.size - i) * 7);
		} else if (data.frequency == 'm') {
			dt.setDate(1);
			dt.setMonth(dt.getMonth() - (periodData.frequencyInfo.size - i));
			dt = getLastDayOfMonth(dt);
		} else if (data.frequency == 'q') {
			dt.setDate(1);
			dt.setMonth(dt.getMonth() - (periodData.frequencyInfo.size - i) * 3);
			dt = getLastDayOfMonth(dt);
		} else if (data.frequency == 'y') {
			dt.setFullYear(dt.getFullYear() - (periodData.frequencyInfo.size - i));
		}
		var dtInfo = getDateValues(dt);
		periodData.endDate.push(dtInfo.value);
		if (i > 0) {
			periodData.labels.push(dtInfo.label);
		}
	}			
		
	return periodData;
}

The function collects one more end date than it does labels because the end date of the previous period is used as the start of the current period. You need to be able to go back one extra period to get the end date of a period that you will not actually be using for the start date of the earliest period that you will.

Once you pull together all of the data for the selected frequency and period, you can then use that data to put together everything else needed for the chart. This takes care of the basics:

var chartData = {};
chartData.title = task.getPlural() + ' assigned to ' + findOption(data.config.groupOptions, data.group).label;
chartData.subtitle = periodData.frequencyInfo.label + ' through ' + periodData.endingDateInfo.label;
chartData.labels = periodData.labels;
chartData.received = [];
chartData.completed = [];
chartData.backlog = [];

… and then all that is left is to loop through all of the periods in the chart to run GlideAggregates to compile all of the actual data:

var task = new GlideAggregate(data.type);
for (var i=1; i<periodData.endDate.length; i++) {
	task.initialize();
	task.addAggregate('COUNT');
	task.addEncodedQuery('assignment_group=' + data.group + '^opened_at>' + periodData.endDate[i-1] + '^opened_at<=' + periodData.endDate[i]);
	task.query();
	task.next();
	chartData.received.push(task.getAggregate('COUNT') * 1);
	task.initialize();
	task.addAggregate('COUNT');
	task.addEncodedQuery('assignment_group=' + data.group + '^closed_at>' + periodData.endDate[i-1] + '^closed_at<=' + periodData.endDate[i]);
	task.query();
	task.next();
	chartData.completed.push(task.getAggregate('COUNT') * 1);
	task.initialize();
	task.addAggregate('COUNT');
	task.addEncodedQuery('assignment_group=' + data.group + '^opened_at<=' + periodData.endDate[i] + '^closed_at>' + periodData.endDate[i] + '^ORclosed_atISEMPTY');
	task.query();
	task.next();
	chartData.backlog.push(task.getAggregate('COUNT') * 1);
}

The last thing we need to deal with is the user making new selections from the four pick lists. That’s a client side issue, so we will need a client side script to detect the selections and call for a refresh of the chart.

function($scope, $rootScope) {
	var c = this;
	$scope.updateChart = function() {
		c.server.update().then(function(response) {
			c.data.config = response.config;
			c.data.group = response.group;
			c.data.type = response.type;
			c.data.frequency = response.frequency;
			c.data.ending = response.ending;
			$rootScope.$broadcast('refresh-workload', {chartObject: response.chartObject});
		});
	}
}

I still want to play around with a few more different and interesting chart types, but there are enough parts and pieces now to warrant the assembly of a version 1.0 Update Set. If I ever get a chance to do more, I can always put out a better one later on.

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.