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.