logo

NJP

Interactive Heat Map: When Are Your Customers Most Active?

Import · Oct 25, 2016 · article

In this post, I am going to outline how I created an interactive heat map in a Service Portal widget by leveraging D3.js. This heat map displays a matrix of colored blocks that indicate how many incidents were created at a given time on a given week day. This could help you forecast service desk scheduling needs, plan when to implement changes, or even identify your highest risk periods.

image

Given that this post is using incident data from my instance, I also added in the capability to click a button and transition the heat map to only focus on a particular incident priority. I'm sure that you can imagine multiple use cases for this heat map, so I suggest you view this post as a framework which you can modify to your own needs.

Since there are previous posts giving a more in-depth introduction to using D3 and Service Portal together, I won't go into much detail with the code. Here are screenshots and/or pasted code for my HTML, CSS, Client Script, and Server Script:

HTML

image

Total Priority 1 Priority 2 Priority 3 Priority 4 Priority 5

CSS

image

rect.bordered {

stroke: #E6E6E6;

stroke-width:2px;

}

text.mono {

font-size: 9pt;

font-family: Consolas, courier;

fill: #aaa;

}

text.axis-workweek {

fill: #000;

}

text.axis-worktime {

fill: #000;

}

.btn {

background-color: white;

border: 1px solid gray !important;

}

.centered-chart {

text-align: center;

}

Client Script

function() {

/* widget controller */

var c = this;

var margin = { top: 50, right: 0, bottom: 100, left: 30 },

width = 960 - margin.left - margin.right,

height = 430 - margin.top - margin.bottom,

gridSize = Math.floor(width / 24),

legendElementWidth = gridSize*2,

buckets = 9,

colors = ["#ffffd9","#edf8b1","#c7e9b4","#7fcdbb","#41b6c4","#1d91c0","#225ea8","#253494","#081d58"],

days = ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"],

times = ["1a", "2a", "3a", "4a", "5a", "6a", "7a", "8a", "9a", "10a", "11a", "12a", "1p", "2p", "3p", "4p", "5p", "6p", "7p", "8p", "9p", "10p", "11p", "12p"];

// Create the chart svg using the defined sizes

var svg = d3.select("#chart").append("svg")

.attr("width", width + margin.left + margin.right)

.attr("height", height + margin.top + margin.bottom)

.append("g")

.attr("transform", "translate(" + margin.left + "," + margin.top + ")");

// Create the update function that takes the data and count property as parameters

c.updateMap = function(data, count) {

// Create day labels

var dayLabels = svg.selectAll(".dayLabel")

.data(days)

.enter().append("text")

.text(function (d) { return d; })

.attr("x", 0)

.attr("y", function (d, i) { return i * gridSize; })

.style("text-anchor", "end")

.attr("transform", "translate(-6," + gridSize / 1.5 + ")")

.attr("class", function (d, i) { return ((i >= 0 && i <= 4) ? "dayLabel mono axis axis-workweek" : "dayLabel mono axis"); });

// Create time labels

var timeLabels = svg.selectAll(".timeLabel")

.data(times)

.enter().append("text")

.text(function(d) { return d; })

.attr("x", function(d, i) { return i * gridSize; })

.attr("y", 0)

.style("text-anchor", "middle")

.attr("transform", "translate(" + gridSize / 2 + ", -6)")

.attr("class", function(d, i) { return ((i >= 7 && i <= 16) ? "timeLabel mono axis axis-worktime" : "timeLabel mono axis"); });

// Creates color scale based on the number of buckets and

var colorScale = d3.scaleQuantile()

.domain([0, buckets - 1, d3.max(data, function (d) { return d[count]; })])

.range(colors);

// Enter and update blocks

var blocks = svg.selectAll(".hour")

.data(data, function(d) {return d.day+':'+d.hour;});

blocks.enter().append("rect")

.attr("x", function(d) { return (d.hour - 1) * gridSize; })

.attr("y", function(d) { return (d.day - 1) * gridSize; })

.attr("rx", 4)

.attr("ry", 4)

.attr("class", "hour bordered")

.attr("width", gridSize)

.attr("height", gridSize)

.style("fill", colors[0])

.transition().duration(1500)

.style("fill", function(d) { return colorScale(d[count]); });

blocks.transition().duration(1500)

.style("fill", function(d) { return colorScale(d[count]); });

// Create the new legend and remove the existing legend

var legend = svg.selectAll(".legend")

.data([0].concat(colorScale.quantiles()), function(d) { return d; });

var legendEnter = legend.enter().append("g")

.attr("class", "legend");

legendEnter.append("rect")

.attr("x", function(d, i) { return legendElementWidth * i; })

.attr("y", height)

.attr("width", legendElementWidth)

.attr("height", gridSize / 2)

.style("fill", function(d, i) { return colors[i]; })

legendEnter.append("text")

.attr("class", "mono")

.text(function(d) { return "≥ " + Math.round(d); })

.attr("x", function(d, i) { return legendElementWidth * i; })

.attr("y", height + gridSize);

legend.exit().remove();

};

// Function that sets the initial blocks while waiting for the server data to be returned

function setInitialBlocks() {

var intialBlocks = [];

for (i=1;i<8;i++) {

for (j=1;j<25;j++) {

intialBlocks.push({"day": i, "hour": j, "total": 0});

}

}

c.updateMap(intialBlocks, "total");

}

// Function to retrieve block data from the server script

c.display = function() {

c.server.update().then(function(data) {

c.updateMap(c.data.blocks, "total");

})

}

setInitialBlocks();

c.display();

}

Server Script

(function() {

if (input) {

var incData = [];

for (i=1;i<8;i++) {

for (j=1;j<25;j++) {

incData.push({"id": i + ":" + j, "day": i, "hour": j, "total": 0, "p1": 0,

"p2": 0, "p3": 0, "p4": 0, "p5": 0});

}

}

// Query the incident table and start totaling the number of records for each priority

var incGR = new GlideRecord('incident');

incGR.orderByDesc('opened_at');

incGR.setLimit(3000);

incGR.query();

while (incGR.next()) {

var hour = incGR.opened_at.slice(11,13);

var gdt = new GlideDateTime(incGR.opened_at);

var day = gdt.getDayOfWeek();

var key = day+':'+hour;

var block = incData.filter(findBlock(key));

if (block) {

block[0].total++;

switch(incGR.priority+'') {

case '1':

block[0].p1++;

break;

case '2':

block[0].p2++;

break;

case '3':

block[0].p3++;

break;

case '4':

block[0].p4++;

break;

case '5':

block[0].p5++;

break;

}

}

}

data.blocks = incData;

}

function findBlock(key) {

return function(element) {

return element.id == key;

}

}

})();

D3 Transitions

By clicking one of the priority buttons, we can transition our heat map to only look at our incidents with that priority. Although screenshots won't demo the aesthetically pleasing, gradual D3 transitions used in our widget, here is an example:

All incidents:

image

Priority 4 incidents:

image

Mitch Stutler

VividCharts Founder

vividcharts.com

linkedin.com/in/mitchellstutler

twitter.com/mitchstutler

Sources

- https://d3js.org/

- Day / Hour Heatmap

View original source

https://www.servicenow.com/community/developer-blog/interactive-heat-map-when-are-your-customers-most-active/ba-p/2282406