“Most of the important things in the world have been accomplished by people who have kept on trying when there seemed to be no hope at all.”
— Dale Carnegie
Long before I had ever heard of ServiceNow, I stumbled across a cool little product called Highcharts. In their own words, “Highcharts is a charting library written in pure JavaScript, offering an easy way of adding interactive charts to your web site or web application.” ServiceNow comes bundled with Highcharts included, which I thought was pretty nifty when I figured that out, because you can do quite a bit with Highcharts with very minimal effort. In fact, that’s one of the things that I really like about both products: both allow you to accomplish quite a bit with just a very small investment of time and energy.
To make things even easier, it always helps to have a few ready-made parts at your disposal to speed things along. Highcharts uses simple Javascript objects to configure their charts, and the contents of those objects can be broken down into two rather distinct categories: 1) elements that control the type, look, and feel of the chart and 2) elements that contain the data to be presented in the chart. Once you design a particular chart with the style, colors, and appearance that you find acceptable, you can save that off and reuse it again and again with different data or data from different periods. For the ServiceNow Service Portal, my thought was to create a generic chart widget that could take any chart configuration object as a passed parameter, and then also create a generic chart configuration object generator that would take a chart type and chart data as parameters and return a chart configuration object that could be then passed to the generic chart widget.
To start things off, I decided to build a simple generic chart widget and pass it a hard-coded chart configuration object, just to prove that everything about the widget was functional. There are a lot of sample Highcharts chart objects floating around the Internet, but for my test I just went out to their Your First Chart tutorial page and grabbed the configuration object for that simple example.
To grab just the chart object, I copied the highlighted code from the page.
Unfortunately, this is not a valid JSON string, which I would need if I was going to pass this around as a parameter, but I could paste it into simple Javascript routine as the value of a variable, and print out a JSON.stringify of that variable and then I ended up with a usable JSON string.
{
"chart":{
"type":"bar"
},
"title":{
"text":"Fruit Consumption"
},
"xAxis":{
"categories":[
"Apples",
"Bananas",
"Oranges"
]
},
"yAxis":{
"title":{
"text":"Fruit eaten"
}
},
"series":[
{
"name":"Jane",
"data":[1, 0, 4]
},{
"name":"John",
"data":[5, 7, 3]
}
]
}
With my new hard-coded chart specification object in hand, I set out to create my generic widget. Every Highchart needs a place to live, so I started out with the HTML portion and created a simple chart DIV inside of a basic panel.
<div class="panel panel-default">
<div class="panel-body form-horizontal">
<div class="col-sm-12">
<div id="chart-container" style="height: 400px;"></div>
</div>
</div>
</div>
On the server side, I wanted to be able to accept a chart configuration object from either input or options, so I ended up with this:
(function() {
if (input && input.chartObject) {
data.chartObject = input.chartObject;
} else {
if (options && options.chart_object) {
try {
data.chartObject = JSON.parse(options.chart_object);
} catch (e) {
gs.addErrorMessage("Unable to parse chart specification: " + e);
}
}
}
})();
On the client side, I also wanted to be able to accept a chart configuration object from a broadcast message from another widget, and I also needed to add the code to actually render the chart, so that turned out to be this:
function ($scope) {
var c = this;
if (c.data.chartObject) {
var myChart = new Highcharts.chart('chart-container', c.data.chartObject);
}
if (c.options.listen_for) {
$scope.$on(c.options.listen_for, function (event, config) {
if (config.chartObject) {
var myChart = new Highcharts.chart('chart-container', config.chartObject);
}
});
}
}
One more thing to do was to set up the Option schema for my two widget options, which is just another JSON string.
[{"hint":"An optional JSON object containing the Chart specifications and data",
"name":"chart_object",
"default_value":"",
"section":"Behavior",
"label":"Chart Object",
"type":"string"},
{"hint":"The name of an Event that will provide a new Chart specification object",
"name":"listen_for",
"default_value":"",
"section":"Behavior",
"label":"Listen for",
"type":"string"}]
Finally, I had to go down to the bottom of the widget page where the related records tabs are found and Edit the dependencies to add Highcharts as a dependency. This will bring in the Highcharts code and make all of the magic happen.
That pretty much completes the widget, so now I just needed to create a page on which to put the widget so that I could test it out. Once I created the page, I was able to pull up the widget options using the pencil icon in the page designer and enter in my JSON object for the chart configuration.
At that point, all that was left was for me to click on the Try it button and look at my beautiful new chart. Unfortunately, all I got to see was a nice big blank chunk of whitespace where my chart should have appeared. Digging into the console error messages, I came across the following:
Highcharts not defined
Well, either my dependency did not load, or my widget couldn’t see the code. I tried $rootScope.Highcharts and $window.Highcharts and $scope.Highcharts, but nothing worked. Then I thought that maybe it was a timing issue and so I put in a setTimeout to wait a few seconds for everything to load, but that didn’t work either. So then I added a script tag to the HTML thinking that maybe the dependency wasn’t pulling in the script after all. Nothing.
At that point, I decided to give up just trying different things on my own and see if I could hunt down some other widget that might already be working with Highcharts and see how those were set up. From the Dependencies list, I used the hamburger menu next to the Dependency column label to select Configure > Table, and then on the Table page, I scrolled down to where I could click on Show list to bring up the list of dependencies. Then I filtered the list for Highcharts to find that there were quite a few existing widgets that pulled in Highcharts as a dependency.
The secret, it appeared, was some additional code in the Link section of the widget, a section that I have always ignored, mainly because I have never understood its purpose or how it worked.
function(scope, element, attrs, controllers) {
scope.$watch('chartOptions', function() {
if (scope.chartOptions) {
$('#chart-container').highcharts(scope.chartOptions);
scope.chart = $('#chart-container').highcharts();
}
});
}
Now this is all AngularJS mystical nonsense sprinkled with Highcharts fairy dust as far as I was concerned, because I have no idea what the Link section of a widget is for or what all of this code actually does. However, it is attached to widgets that actually work, so I got busy cutting and pasting. The other thing that I had to do based on the examples that I was examining was to modify my client-side code a little, which now looked like this:
function ($scope) {
var c = this;
if (c.data.chartObject) {
$scope.chartOptions = c.data.chartObject;
}
if (c.options.listen_for) {
$scope.$on(c.options.listen_for, function (event, config) {
if (config.chartObject) {
$scope.chartOptions = config.chartObject;
}
});
}
}
Yes, we are now deep into the realm of Cargo Cult Programming, but I’ve never been too proud to copy other people’s working code, even if I didn’t understand it. Now let’s hit that Try it button one more time …
Well, look at that! I’m not sure how all of that works, but it does work, so I’m good. Now that I have my functioning generic chart widget, I can start working on my chart object generator and then see if I can’t wire the two of them together somehow …