mitchellstutler
Tera Contributor

In this post, I will outline how I was able to create a treemap in a Service Portal widget using D3.js. Treemaps convert hierarchical data into a conglomeration of nested rectangles that represent the data values.

This particular widget example will query the ServiceNow catalog categories and catalog items that are in my personal developer instance to generate the data object that will be visually expressed in my D3 treemap. As a business use case, you would probably want to query the Requested Item table to display which items and categories are the most frequently ordered. My developer instance doesn't have much Requested Item data, so I generated random numbers to better display the treemap functionality.

Below is a screenshot of my treemap widget in action:

D3 Treemap.png

Each color represents a single catalog category and each rectangle represents a catalog item. The size of the catalog item rectangle is scaled according to how many times that item has been ordered. The bigger the rectangle, the more that item has been ordered. I also added the ability to resize the rectangles to equal sizes to display category sizes based on how many items live under it. To change between these two views, I set up radio buttons to trigger the transition. Below is a screenshot of the second view:

D3 Treemap Categories.png

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 is the pasted code for my HTML, CSS, Client Script, and Server Script:

HTML

<div class="centered-chart">

        <h1>D3 Treemap</h1>

        <svg width="960" height="570"></svg>

</div>

<form>

        <label><input type="radio" name="mode" value="sumBySize" checked> Ordered Count</label>

        <label><input type="radio" name="mode" value="sumByCount"> Category Size</label>

</form>

CSS

form {
    padding-left: 150px;
    font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
}

.centered-chart {
    text-align: center;
    font: 10px sans-serif;
}

Client Script

function() {

                      /* widget controller */

                      var c = this;

                 

                      // Grab our category object from the data object

                      var categories = c.data.categories;

                 

                      var svg = d3.select("svg"),

                      width = +svg.attr("width"),

                      height = +svg.attr("height");

                 

                      var fader = function(color) { return d3.interpolateRgb(color, "#fff")(0.2); },

                      color = d3.scaleOrdinal(d3.schemeCategory20.map(fader)),

                      format = d3.format(",d");

                                         

                      // Define our D3 treemap object

                      var treemap = d3.treemap()

                      .tile(d3.treemapResquarify)

                      .size([width, height])

                      .round(true)

                      .paddingInner(1);

                 

                      // Define function that draws treemap based on the data parameter

                      function loadData(data) {

                                         

                                              var root = d3.hierarchy(data)

                                              .eachBefore(function(d) {

                                                                      d.data.id = (d.parent ? d.parent.data.id + "." : "") + d.data.name;

                                                                      d.data.title = (d.parent ? d.parent.data.name + " > " : "") + d.data.name;

                                              })

                                              .sum(sumBySize)

                                              .sort(function(a, b) { return b.height - a.height || b.value - a.value; });

                                         

                                              treemap(root);

                                         

                                              var cell = svg.selectAll("g")

                                              .data(root.leaves())

                                              .enter().append("g")

                                              .attr("transform", function(d) { return "translate(" + d.x0 + "," + d.y0 + ")"; });

                                         

                                              cell.append("rect")

                                              .attr("id", function(d) { return d.data.id; })

                                              .attr("width", function(d) { return parseInt(d.x1 - d.x0); })

                                              .attr("height", function(d) { return parseInt(d.y1 - d.y0); })

                                              .attr("fill", function(d) { return color(d.parent.data.id); });

                                         

                                              cell.append("clipPath")

                                              .attr("id", function(d) { return "clip-" + d.data.id; })

                                              .append("use")

                                              .attr("xlink:href", function(d) { return "#" + d.data.id; });

                                         

                                              cell.append("text")

                                              .attr("clip-path", function(d) { return "url(#clip-" + d.data.id + ")"; })

                                              .selectAll("tspan")

                                              .data(function(d) { return d.data.name.split(/(?=[A-Z][^A-Z])/g); })

                                              .enter().append("tspan")

                                              .attr("x", 4)

                                              .attr("y", function(d, i) { return 13 + i * 10; })

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

                                         

                                              cell.append("title")

                                              .text(function(d) { return d.data.title + "\n" + format(d.value) + " Requested"; });

                                         

                                              d3.selectAll("input")

                                              .data([sumBySize, sumByCount], function(d) { return d ? d.name : this.value; })

                                              .on("change", changed);

                                         

                                              // Set timeout that will automatically change our treemap to demonstrate transitions

                                              var timeout = d3.timeout(function() {

                                                                      d3.select("input[value=\"sumByCount\"]")

                                                                      .property("checked", true)

                                                                      .dispatch("change");

                                              }, 2000);

                                         

                                              function changed(sum) {

                                                                      timeout.stop();

                                                                 

                                                                      treemap(root.sum(sum));

                                                                 

                                                                      cell.transition()

                                                                      .duration(750)

                                                                      .attr("transform", function(d) { return "translate(" + d.x0 + "," + d.y0 + ")"; })

                                                                      .select("rect")

                                                                      .attr("width", function(d) {

                                                                                              var width = parseInt(d.x1 - d.x0);

                                                                                              console.log(typeof width);

                                                                                              return width;

                                                                      })

                                                                      .attr("height", function(d) {

                                                                                              var height = parseInt(d.y1 - d.y0);

                                                                                              console.log(typeof height);

                                                                                              return height;

                                                                      });

                                              }

                      }

                 

                      // Call our data load function to initially draw the treemap with our data object

                      loadData(categories);

                 

                      function sumByCount(d) {

                                              return d.children ? 0 : 1;

                      }

                 

                      function sumBySize(d) {

                                              return d.size;

                      }

                 

}

Server Script

(function() {

/* populate the 'data' object */

// Query catalog items in the Service Catalog

var catGR = new GlideRecord('sc_cat_item');

catGR.addActiveQuery();

// I hardcoded the sys_id of the Service Catalog here. This could definitely

// be dynamically set up as a widget option.

catGR.addQuery('sc_catalogs', 'e0d08b13c3330100c8b837659bba8fb4');

catGR.addNotNullQuery('category.title');

catGR.orderBy('category');

catGR.query();

                   

// Declare our object that will contain the item data

var cats = {

"name": "Service Catalog",

"children": []

}

                   

var previousCat = '';

var tempArray = [];

                   

// Loop through items and populate our cats object according to the

// category structure. I don't have great RITM data in my personal

// dev instance, so I used random numbers to imitate the counts.

while (catGR.next()) {

var category = catGR.category.title+'';

                                           

if (previousCat == category)

tempArray.push({"name": catGR.name+'', "size": Math.floor((Math.random() * 100) + 1)});

else {

cats.children.push({"name": previousCat, "children": tempArray});

tempArray = [];

}

previousCat = category;

}

                   

// Pass our category object to the data object to be used client side

data.categories = cats;

                   

})();

Mitch Stutler

VividCharts Founder

vividcharts.com

linkedin.com/in/mitchellstutler

twitter.com/mitchstutler

Sources

- d3js.org

- Treemap