mitchellstutler
Tera Contributor

In this post, I am going to outline how I created an interactive scatter plot in a Service Portal widget by leveraging D3.js. For this example, we are going to create the scatter plot using project records that are stored in our ServiceNow instance. What I would really like to highlight in this post is how you can use different data channels to tell different stories.

This particular scatter plot uses the following channels (how a particular shape can visually display data) to demonstrate different project attributes:

  1. X-axis displays the planned duration of the project
  2. Y-axis displays the planned cost of the project
  3. The size of each circle displays the planned effort of the project relative to the other projects displayed
  4. The color of each circle displays the priority of the project
  5. The line of best fit displays where the project sits in relation to the other projects in terms of planned duration vs. planned cost

Here is a full screenshot of this scatter plot to give a better idea of how these channels are being used:

Post 6 Scatter Plot.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 row">

  <div id="chart"></div>

</div>

CSS

.axis path,

.axis line {

  fill: none;

  stroke: #000;

  shape-rendering: crispEdges;

}

.dot {

  stroke: gray;

}

.dot:hover {

  stroke: black;

}

.centered-chart {

  text-align: center;

  font: 10px sans-serif;

}

.p1 {

  fill: #FF6347;

}

.p2 {

  fill: #FFA500;

}

.p3 {

  fill: #FFD700;

}

.p4 {

  fill: #87CEFA;

}

.p5 {

  fill: #90EE90;

}

Client Script

function() {

                      /* widget controller */

                      var c = this;

               

                      var data = c.data.projects;

               

                      var margin = {top: 20, right: 75, bottom: 75, left: 75},

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

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

                      minCircle = 2, maxCircle = 20;

               

                      var maxDuration = d3.max(data, function(d) { return d.duration; });

                      var minDuration = d3.min(data, function(d) { return d.duration; });

               

                      // Set the colors used for each priority

                      var priorities = [{"priority": 1, "color": "#FF6347"},

                      {"priority": 2, "color": "#FFA500"},

                      {"priority": 3, "color": "#FFD700"},

                      {"priority": 4, "color": "#87CEFA"},

                      {"priority": 5, "color": "#90EE90"}];

               

                      // Set the scale for the x-axis

                      var x = d3.scaleLinear()

                      .range([0, width])

                      .domain([d3.min(data, function(d) { return d.duration; }) - 2, d3.max(data, function(d) { return d.duration; }) + 2]);

               

                      // Set the scale for the y-axis

                      var y = d3.scaleLinear()

                      .range([height, 0])

                      .domain([d3.min(data, function(d) { return d.cost; }) - 10000, d3.max(data, function(d) { return d.cost; }) + 10000]);

               

                      // Set the scale for the circle size

                      var r = d3.scaleLinear()

                      .domain([ 0, d3.max(data, function(d) {return d.effort;}) ]).range([ minCircle, maxCircle ]);

               

                      var xAxis = d3.axisBottom().scale(x);

                      var yAxis = d3.axisLeft().scale(y);

               

                      // Create an svg canvas for our chart

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

                      .attr("id", "chart-container")

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

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

                      .append("g")

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

               

                      // Create a circle for each of the project records

                      function generateCircles() {

                                              svg.selectAll(".dot")

                                              .data(data)

                                              .enter().append("circle")

                                              .attr("class", function(d) { return "dot p" + d.priority; })

                                              .attr("r", function(d) { return r(d.effort); })

                                              .attr("cx", function(d) { return x(d.duration); })

                                              .attr("cy", function(d) { return y(d.cost); })

                                              .on("mouseover",function(d,i){

                                                                      this.parentNode.appendChild(this);

                                                                      d3.select("#tooltip").attr("transform", "translate("+(x(d.duration)+20)+","+(y(d.cost)+20)+")");

                                                                      d3.select("#duration").text("Duration: " + d.duration);

                                                                      d3.select("#cost").text("Cost: $" + d.cost);

                                                                      d3.select("#effort").text("Effort: " + d.effort + ' hours');

                                              })

                                              .on("mouseout",function(d,i){

                                                                      d3.selectAll(".tooltip-attribute").text("");

                                              });

                      }

               

                      // Create the priority legend

                      function generatePriorityLegend() {

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

                                              .data(priorities)

                                              .enter().append("g")

                                              .attr("class", "legend")

                                              .attr("transform", function(d, i) { return "translate(0," + i * 20 + ")"; });

                                       

                                              legend.append("rect")

                                              .attr("x", width + 20)

                                              .attr("width", 18)

                                              .attr("height", 18)

                                              .style("fill", function(d) { return d.color; });

                                       

                                              legend.append("text")

                                              .attr("x", width + 14)

                                              .attr("y", 9)

                                              .attr("dy", ".35em")

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

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

                                       

                                              var priorityLabel = d3.select("#chart-container")

                                              .append("g")

                                              .attr("transform", "translate(1130,55)")

                                              .append("text")

                                              .attr("transform", "rotate(-90)")

                                              .attr("y", 6)

                                              .attr("dy", ".71em")

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

                                              .text("Priority");

                      }

               

                      // Create initial tooltip

                      function generateTooltip() {

                                              var tooltip = svg.append("g")

                                              .append("g")

                                              .attr("id", "tooltip");

                                       

                                              tooltip.append("text")

                                              .attr("id", "duration")

                                              .attr("class", "tooltip-attribute");

                                       

                                              tooltip.append("text")

                                              .attr("id", "cost")

                                              .attr("y", 15)

                                              .attr("class", "tooltip-attribute");

                                       

                                              tooltip.append("text")

                                              .attr("id", "effort")

                                              .attr("y", 30)

                                              .attr("class", "tooltip-attribute");

                      }

               

                      // Add the x and y axes and their labels

                      function generateAxes() {

                                              svg.append("g")

                                              .attr("class", "x axis")

                                              .attr("transform", "translate(0," + height + ")")

                                              .call(xAxis);

                                       

                                              svg.append("text")

                                              .attr("x", width)

                                              .attr("y", height - 6)

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

                                              .text("Duration (Days)");

                                       

                                              svg.append("g")

                                              .attr("class", "y axis")

                                              .call(yAxis);

                                       

                                              svg.append("text")

                                              .attr("transform", "rotate(-90)")

                                              .attr("y", 6)

                                              .attr("dy", ".71em")

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

                                              .text("Cost ($)");

                      }

               

                      // Create the line of best fit

                      function generateLine() {

                                              var linReg =     linearRegression(data);

                                       

                                              var myLine = svg.append("svg:line")

                                              .attr("x1", x(minDuration))

                                              .attr("y1", y(minDuration*linReg.slope + linReg.intercept))

                                              .attr("x2", x(maxDuration))

                                              .attr("y2", y( (maxDuration*linReg.slope) + linReg.intercept ))

                                              .style("stroke", "#A9A9A9");

                                       

                                              function linearRegression(projects){

                                                                      var lr = {};

                                                                      var n = projects.length;

                                                                      var sum_x = 0;

                                                                      var sum_y = 0;

                                                                      var sum_xy = 0;

                                                                      var sum_xx = 0;

                                                                      var sum_yy = 0;

                                                               

                                                                      for (var i = 0; i < projects.length; i++) {

                                                                                              sum_x += projects[i].duration;

                                                                                              sum_y += projects[i].cost;

                                                                                              sum_xy += (projects[i].duration*projects[i].cost);

                                                                                              sum_xx += (projects[i].duration*projects[i].duration);

                                                                                              sum_yy += (projects[i].cost*projects[i].cost);

                                                                      }

                                                               

                                                                      lr.slope = (n * sum_xy - sum_x * sum_y) / (n*sum_xx - sum_x * sum_x);

                                                                      lr.intercept = (sum_y - lr.slope * sum_x)/n;

                                                                      lr.r2 = Math.pow((n*sum_xy - sum_x*sum_y)/

                                                                      Math.sqrt((n*sum_xx-sum_x*sum_x)*(n*sum_yy-sum_y*sum_y)),2);

                                                               

                                                                      return lr;

                                              }

                      }

               

                      generateCircles();

                      generatePriorityLegend();

                      generateTooltip();

                      generateAxes();

                      generateLine();

}

Server Script

(function() {

                      var projData = [];

               

                      // Grab projects in the IT porfolio

                      var projGR = new GlideRecord('pm_project');

                      projGR.addQuery('primary_portfolio', '30e14b3beb131100b749215df106fe0f');

                      projGR.addEncodedQuery("resource_planned_cost>javascript:getCurrencyFilter('pm_project','resource_planned_cost', 'USD;0')^effort>javascript:gs.getDurationDate('0 0:0:0')");

                      projGR.orderByDesc('start_date');

                      projGR.query();

               

                      // Push each project record into an array to pass to the client script

                      while (projGR.next()) {

                                              var dc = new DurationCalculator();

                                              var hours = dc.calcScheduleDuration('1970-01-01 00:00:00', projGR.effort)/60/60;

                                              var duration = dc.calcScheduleDuration('1970-01-01 00:00:00', projGR.duration)/60/60/24;

                                              projData.push({

                                                                      "name": projGR.short_description+'',

                                                                      "cost": +projGR.cost,

                                                                      "duration": +duration,

                                                                      "effort": +hours,

                                                                      "priority": projGR.priority+''

                                              });

                      }

               

                      data.projects = projData;

               

})();

Mitch Stutler

VividCharts Founder

vividcharts.com

linkedin.com/in/mitchellstutler

twitter.com/mitchstutler

Sources