How To Guide:
Intro to Visualizations

Building Visualizations in D3

Sample Builds

Currently I'm working on a walkthrough of the SCN Gauge tutorial from SAP with notes on the differences between D3 in the browser and Design Studio. I also have plans to update this later with a more general walkthrough on how to convert an existing D3 visualization to a Design Studio extension.

D3

Scatterplot
Bar Graph

SAP Design Studio SDK

For comparison, here is an example of D3 being used in Design Studio. The syntax is slightly different, but you can see the similarities. It's especially noticeable for this example because they've taken the time to set up an object for it. Examples which have this done already are more easily modifiable into Design Studio extensions.

Browser D3 - Gauges
SAP Tutorial on Creating an Extension with D3 - Gauges

var gauges = [];

function createGauge(name, label, min, max)
{
  var config = 
  {
    size: 120,
    label: label,
    min: undefined != min ? min : 0,
    max: undefined != max ? max : 100,
    minorTicks: 5
  }
  
  var range = config.max - config.min;
  config.yellowZones = [{ from: config.min + range*0.75, to: config.min + range*0.9 }];
  config.redZones = [{ from: config.min + range*0.9, to: config.max }];
  
  gauges[name] = new Gauge(name + "GaugeContainer", config);
  gauges[name].render();
}

function createGauges()
{
  createGauge("memory", "Memory");
  createGauge("cpu", "CPU");
  createGauge("network", "Network");
  //createGauge("test", "Test", -50, 50 );
}

function updateGauges()
{
  for (var key in gauges)
  {
    var value = getRandomValue(gauges[key])
    gauges[key].redraw(value);
  }
}

function getRandomValue(gauge)
{
  var overflow = 0; //10;
  return gauge.config.min - overflow + (gauge.config.max - gauge.config.min + overflow*2) *  Math.random();
}

function initialize()
{
  createGauges();
  setInterval(updateGauges, 5000);
}

function Gauge(placeholderName, configuration)
{
  this.placeholderName = placeholderName;
  
  var self = this; // for internal d3 functions
  
  this.configure = function(configuration)
  {
    this.config = configuration;
    
    this.config.size = this.config.size * 0.9;
    
    this.config.raduis = this.config.size * 0.97 / 2;
    this.config.cx = this.config.size / 2;
    this.config.cy = this.config.size / 2;
    
    this.config.min = undefined != configuration.min ? configuration.min : 0; 
    this.config.max = undefined != configuration.max ? configuration.max : 100; 
    this.config.range = this.config.max - this.config.min;
    
    this.config.majorTicks = configuration.majorTicks || 5;
    this.config.minorTicks = configuration.minorTicks || 2;
    
    this.config.greenColor   = configuration.greenColor || "#109618";
    this.config.yellowColor = configuration.yellowColor || "#FF9900";
    this.config.redColor   = configuration.redColor || "#DC3912";
    
    this.config.transitionDuration = configuration.transitionDuration || 500;
  }

  this.render = function()
  {
    this.body = d3.select("#" + this.placeholderName)
                  .append("svg:svg")
                  .attr("class", "gauge")
                  .attr("width", this.config.size)
                  .attr("height", this.config.size);
    
    this.body.append("svg:circle")
             .attr("cx", this.config.cx)
             .attr("cy", this.config.cy)
             .attr("r", this.config.raduis)
             .style("fill", "#ccc")
             .style("stroke", "#000")
             .style("stroke-width", "0.5px");
          
    this.body.append("svg:circle")
             .attr("cx", this.config.cx)
             .attr("cy", this.config.cy)
             .attr("r", 0.9 * this.config.raduis)
             .style("fill", "#fff")
             .style("stroke", "#e0e0e0")
             .style("stroke-width", "2px");
          
    for (var index in this.config.greenZones)
    {
      this.drawBand(this.config.greenZones[index].from, this.config.greenZones[index].to, self.config.greenColor);
    }
    
    for (var index in this.config.yellowZones)
    {
      this.drawBand(this.config.yellowZones[index].from, this.config.yellowZones[index].to, self.config.yellowColor);
    }
    
    for (var index in this.config.redZones)
    {
      this.drawBand(this.config.redZones[index].from, this.config.redZones[index].to, self.config.redColor);
    }
    
    if (undefined != this.config.label)
    {
      var fontSize = Math.round(this.config.size / 9);
      this.body.append("svg:text")
               .attr("x", this.config.cx)
               .attr("y", this.config.cy / 2 + fontSize / 2)
               .attr("dy", fontSize / 2)
               .attr("text-anchor", "middle")
               .text(this.config.label)
               .style("font-size", fontSize + "px")
               .style("fill", "#333")
               .style("stroke-width", "0px");
    }
    
    var fontSize = Math.round(this.config.size / 16);
    var majorDelta = this.config.range / (this.config.majorTicks - 1);
    for (var major = this.config.min; major <= this.config.max; major += majorDelta)
    {
      var minorDelta = majorDelta / this.config.minorTicks;
      for (var minor = major + minorDelta; minor < Math.min(major + majorDelta, this.config.max); minor += minorDelta)
      {
        var point1 = this.valueToPoint(minor, 0.75);
        var point2 = this.valueToPoint(minor, 0.85);
        
        this.body.append("svg:line")
                 .attr("x1", point1.x)
                 .attr("y1", point1.y)
                 .attr("x2", point2.x)
                 .attr("y2", point2.y)
                 .style("stroke", "#666")
                 .style("stroke-width", "1px");
      }
      
      var point1 = this.valueToPoint(major, 0.7);
      var point2 = this.valueToPoint(major, 0.85);  
      
      this.body.append("svg:line")
               .attr("x1", point1.x)
               .attr("y1", point1.y)
               .attr("x2", point2.x)
               .attr("y2", point2.y)
               .style("stroke", "#333")
               .style("stroke-width", "2px");
      
      if (major == this.config.min || major == this.config.max)
      {
        var point = this.valueToPoint(major, 0.63);
        
        this.body.append("svg:text")
                 .attr("x", point.x)
                 .attr("y", point.y)
                 .attr("dy", fontSize / 3)
                 .attr("text-anchor", major == this.config.min ? "start" : "end")
                 .text(major)
                 .style("font-size", fontSize + "px")
                 .style("fill", "#333")
                 .style("stroke-width", "0px");
      }
    }
    
    var pointerContainer = this.body.append("svg:g").attr("class", "pointerContainer");
    
    var midValue = (this.config.min + this.config.max) / 2;
    
    var pointerPath = this.buildPointerPath(midValue);
    
    var pointerLine = d3.svg.line()
                        .x(function(d) { return d.x })
                        .y(function(d) { return d.y })
                        .interpolate("basis");
    
    pointerContainer.selectAll("path")
                    .data([pointerPath])
                    .enter()
                    .append("svg:path")
                    .attr("d", pointerLine)
                    .style("fill", "#dc3912")
                    .style("stroke", "#c63310")
                    .style("fill-opacity", 0.7)
          
    pointerContainer.append("svg:circle")
                    .attr("cx", this.config.cx)
                    .attr("cy", this.config.cy)
                    .attr("r", 0.12 * this.config.raduis)
                    .style("fill", "#4684EE")
                    .style("stroke", "#666")
                    .style("opacity", 1);
    
    var fontSize = Math.round(this.config.size / 10);
    pointerContainer.selectAll("text")
                    .data([midValue])
                    .enter()
                    .append("svg:text")
                    .attr("x", this.config.cx)
                    .attr("y", this.config.size - this.config.cy / 4 - fontSize)
                    .attr("dy", fontSize / 2)
                    .attr("text-anchor", "middle")
                    .style("font-size", fontSize + "px")
                    .style("fill", "#000")
                    .style("stroke-width", "0px");
    
    this.redraw(this.config.min, 0);
  }
  
  this.buildPointerPath = function(value)
  {
    var delta = this.config.range / 13;
    
    var head = valueToPoint(value, 0.85);
    var head1 = valueToPoint(value - delta, 0.12);
    var head2 = valueToPoint(value + delta, 0.12);
    
    var tailValue = value - (this.config.range * (1/(270/360)) / 2);
    var tail = valueToPoint(tailValue, 0.28);
    var tail1 = valueToPoint(tailValue - delta, 0.12);
    var tail2 = valueToPoint(tailValue + delta, 0.12);
    
    return [head, head1, tail2, tail, tail1, head2, head];
    
    function valueToPoint(value, factor)
    {
      var point = self.valueToPoint(value, factor);
      point.x -= self.config.cx;
      point.y -= self.config.cy;
      return point;
    }
  }
  
  this.drawBand = function(start, end, color)
  {
    if (0 >= end - start) return;
    
    this.body.append("svg:path")
             .style("fill", color)
             .attr("d", d3.svg.arc()
             .startAngle(this.valueToRadians(start))
             .endAngle(this.valueToRadians(end))
             .innerRadius(0.65 * this.config.raduis)
             .outerRadius(0.85 * this.config.raduis))
             .attr("transform", function() { return "translate(" + self.config.cx + ", " + self.config.cy + ") rotate(270)" });
  }
  
  this.redraw = function(value, transitionDuration)
  {
    var pointerContainer = this.body.select(".pointerContainer");
    
    pointerContainer.selectAll("text").text(Math.round(value));
    
    var pointer = pointerContainer.selectAll("path");
    pointer.transition()
          .duration(undefined != transitionDuration ? transitionDuration : this.config.transitionDuration)
          //.delay(0)
          //.ease("linear")
          //.attr("transform", function(d) 
          .attrTween("transform", function()
          {
            var pointerValue = value;
            if (value > self.config.max) pointerValue = self.config.max + 0.02*self.config.range;
            else if (value < self.config.min) pointerValue = self.config.min - 0.02*self.config.range;
            var targetRotation = (self.valueToDegrees(pointerValue) - 90);
            var currentRotation = self._currentRotation || targetRotation;
            self._currentRotation = targetRotation;
            
            return function(step) 
            {
              var rotation = currentRotation + (targetRotation-currentRotation)*step;
              return "translate(" + self.config.cx + ", " + self.config.cy + ") rotate(" + rotation + ")"; 
            }
          });
  }
  
  this.valueToDegrees = function(value)
  {
    // thanks @closealert
    //return value / this.config.range * 270 - 45;
    return value / this.config.range * 270 - (this.config.min / this.config.range * 270 + 45);
  }
  
  this.valueToRadians = function(value)
  {
    return this.valueToDegrees(value) * Math.PI / 180;
  }
  
  this.valueToPoint = function(value, factor)
  {
    return {   x: this.config.cx - this.config.raduis * factor * Math.cos(this.valueToRadians(value)),
               y: this.config.cy - this.config.raduis * factor * Math.sin(this.valueToRadians(value))     };
  }
  
  // initialization
  this.configure(configuration);  
  
}}

sap.designstudio.sdk.Component.subclass("com.sap.sample.scngauge.SCNGauge", function() {

  var me = this;
  //Properties
  me._colorCode = 'blue';
  me._innerRad = 0.0;
  me._outerRad = 0.0;
  me._endAngleDeg = 90.0;
  me._startAngleDeg = -90.0;
  me._paddingTop = 0;
  me._paddingBottom = 0;
  me._paddingLeft = 0;
  me._paddingRight = 0;
  me._offsetLeft = 0;
  me._offsetDown = 0;
  
  //New with Part 6
  me._useMeasures = false;
  me._endAngleDegMax = 90.0;
  me._measureMax = 0;
  me._measureMin = 0;
  me._measureVal = 0;
  
  //Validate the Inner and Outer Radii
  me.validateRadii = function(inner, outer) {
    if (inner <= outer) {
      return true;
    } else {
      return false;
    }
  };
  
  
  //Recalculate Outer Radius.  Also, double check that the new value fits with me._innerRad
  me.recalculateOuterRadius = function(paddingLeft, paddingRight, paddingTop, paddingBottom){
    // Find the larger left/right padding
    var lrPadding = paddingLeft + paddingRight;
    var tbPadding = paddingTop + paddingBottom;
    var maxPadding = lrPadding;
    if (maxPadding < tbPadding){
      maxPadding = tbPadding
    }      
    var newOuterRad = (me.$().width() - 2*(maxPadding))/2;
    var isValid = me.validateRadii(me._innerRad, newOuterRad);
    if (isValid === true){
      me._outerRad = newOuterRad;
      return true;
    }
    else {
      return false;
    }
  }
  
  //Getters and Setters
  me.colorCode = function(value) {
    if (value === undefined) {
      return me._colorCode;
    } else {
      me._colorCode = value;
      me.redraw();
      return me;
    }
  };
  
  me.innerRad = function(value) {
    if (value === undefined) {
      return me._innerRad;
    } else {
    
      var isValid = me.validateRadii(value, me._outerRad);
      if (isValid === false){
        alert("Warning!  The gauge arc can't have a small inner radius than outer!  Inner Radius must be equal to or less than " + me._outerRad);
        alert("Please decrease the inner radius, or increase the size of the control.  Height & width (including subtraction for padding) must me at least twice as large as Internal Radius!");
      } else {
        me._innerRad = value;
        me.redraw();
      }
      return this;
    }
  };
  
  me.endAngleDeg = function(value) {
    if (value === undefined) {
      return me._endAngleDeg;
    } else {
      me._endAngleDeg = value;
      me.recalculateCurrentAngle();
      return this;
    }
  };

  me.startAngleDeg = function(value) {
    if (value === undefined) {
      return me._startAngleDeg;
    } else {
      me._startAngleDeg = value;
      me.recalculateCurrentAngle();
      return this;
    }
  };
  
  me.currentAngle = function(value) {
    if (value === undefined) {
      return me._endAngleDeg;
    } else {
      me._endAngleDeg = value;
      me.recalculateCurrentAngle();
      return this;
    }
  };
  
  me.paddingTop = function(value) {
    if (value === undefined) {
      return me._paddingTop;
    } else {
      var isValid =me.recalculateOuterRadius(me._paddingLeft, me._paddingRight, value, me._paddingBottom);
      if (isValid === false){
        alert("Warning!  The gauge arc can't have a small inner radius than outer!  Outer Radius must be equal to or greater than " + me._innerRad);
        alert("Please decrease the inner radius, or increase the size of the control.  Height & width (including subtraction for padding) must me at least twice as large as Internal Radius!");
      } else {
        me._paddingTop = value;
        me.redraw();
      }
      return this;
    }
  };
  
  me.paddingBottom = function(value) {
    if (value === undefined) {
      return me._paddingBottom;
    } else {
      var isValid = me.recalculateOuterRadius(me._paddingLeft, me._paddingRight, me._paddingTop, value);
      if (isValid === false){
        alert("Warning!  The gauge arc can't have a small inner radius than outer!  Outer Radius must be equal to or greater than " + me._innerRad);
        alert("Please decrease the inner radius, or increase the size of the control.  Height & width (including subtraction for padding) must me at least twice as large as Internal Radius!");
      } else {
        me._paddingBottom = value;
        me.redraw();
      }
      return this;
    }
  };
  
  me.paddingLeft = function(value) {
    if (value === undefined) {
      paddingLeft = me._paddingLeft;
      return paddingLeft;
    } else {
      var isValid = me.recalculateOuterRadius(value, me._paddingRight, me._paddingTop, me._paddingBottom);
      if (isValid === false){
        alert("Warning!  The gauge arc can't have a small inner radius than outer!  Outer Radius must be equal to or greater than " + me._innerRad);
        alert("Please decrease the inner radius, or increase the size of the control.  Height & width (including subtraction for padding) must me at least twice as large as Internal Radius!");
      } else {
        me._paddingLeft = value;
        me.redraw();
      }
      return this;

    }
  };
  
  me.paddingRight = function(value) {
    if (value === undefined) {
      paddingRight = me._paddingRight;
    } else {
      var isValid = me.recalculateOuterRadius(me._paddingLeft, value, me._paddingTop, me._paddingBottom);
      if (isValid === false){
        alert("Warning!  The gauge arc can't have a small inner radius than outer!  Outer Radius must be equal to or greater than " + me._innerRad);
        alert("Please decrease the inner radius, or increase the size of the control.  Height & width (including subtraction for padding) must me at least twice as large as Internal Radius!");
      } else {
        me._paddingRight = value;
        me.redraw();
      }
      return this;
    }
  };
  
  //New with Part 6 
  me.useMeasures = function(value) {
    if (value === undefined) {
      return me._useMeasures;
    } else {
      me._useMeasures = value;
      me.recalculateCurrentAngle();
      return this;
    }
  };
  
  me.endAngleDegMax = function(value) {
    if (value === undefined) {
      return me._endAngleDegMax;
    } else {
      me._endAngleDegMax = value;
      me.recalculateCurrentAngle();
      return this;
    }
  };

  
  me.measureMax = function(value) {
    if (value === undefined) {
      return me._measureMax;
    } else {
      if (value >= me._measureMin){
        me._measureMax = value;
        me.recalculateCurrentAngle();
      }
      else{
        alert("The maximum displayed value of the measure must be greater then the minimum!");
      }
      return this;
    }
  };
  
  me.measureMin = function(value) {
    if (value === undefined) {
      return me._measureMin;
    } else {
      if (value <= me._measureMax){
        me._measureMin = value;
        me.recalculateCurrentAngle();
      }
      else{
        alert("The maximum displayed value of the measure must be greater then the minimum!");
      }
      return this;
    }
  };
  
  me.measureVal = function(value) {
    if (value === undefined) {
      return me._measureVal;
    } else {
      me._measureVal = value;
      me.recalculateCurrentAngle();
      return this;
    }
  };
  
  
  me.redraw = function() {

    var myDiv = me.$()[0];
    
    // Clear any existing gauges.  We'll redraw from scratch
    d3.select(myDiv).selectAll("*").remove();  
    
    var vis = d3.select(myDiv).append("svg:svg").attr("width", "100%").attr("height", "100%");
    var pi = Math.PI;
    
    // Find the larger left/right padding
    var lrPadding = me._paddingLeft + me._paddingRight;
    var tbPadding = me._paddingTop + me._paddingBottom;
    var maxPadding = lrPadding;
    if (maxPadding < tbPadding){
      maxPadding = tbPadding
    }      
    
    me._outerRad = (me.$().width() - 2*(maxPadding))/2;
    
    //Don't let the innerRad be greater than outer rad
    if (me._outerRad <= me._innerRad){
      alert("Warning!  The gauge arc can't have a negative radius!  Please decrease the inner radius, or increase the size of the control.  Height & width (including subtraction for padding) must me at least twice as large as Internal Radius!");
    } 
    
    //The offset will determine where the center of the arc shall be
    me._offsetLeft = me._outerRad + me._paddingLeft;
    me._offsetDown = me._outerRad + me._paddingTop;
    
    var arcDef = d3.svg.arc()
      .innerRadius(me._innerRad)
      .outerRadius(me._outerRad)
      .startAngle(me._startAngleDeg * (pi/180)) //converting from degs to radians
      .endAngle(me._endAngleDeg * (pi/180)); //converting from degs to radians

    var guageArc = vis.append("path")
        .style("fill", me._colorCode)
        .attr("width", me.$().width()).attr("height", me.$().height()) // Added height and width so arc is visible
        .attr("transform", "translate(" + me._offsetLeft + "," + me._offsetDown + ")")
        .attr("d", arcDef);
  };
  
  
  me.init = function() {
    me.redraw();
  };
  
  
  //Getters for the height and width of the component
  me.getWidth = function(){
    return me.$().width();
  };
  
  me.getHeight = function(){
    return me.$().height();
  };
  
  //New with Part 6
  me.recalculateCurrentAngle = function(){
    if (me._useMeasures == true){
      //Firstly, ensure that we can turn in a clockwise manner to get from startAngleDeg to endAngleDegMax
      while (me._endAngleDeg < me._startAngleDeg){
        me._endAngleDegMax = me.me._endAngleDegMax + 360.0;
      }
      
      var currEnd = 0.0;
      if (me._measureVal > me._measureMax){
        currEnd = me._endAngleDegMax;
      } 
      else if (me._measureVal  < me._measureMin){
        currEnd = me._startAngleDeg;
      } else{
        var measureDelta = me._measureMax - me._measureMin;
        var measureValNormalized = 0.0;
        if (measureDelta >  measureValNormalized){
          var measureValNormalized = me._measureVal / measureDelta;
        }
        currEnd = me._startAngleDeg + (measureValNormalized * (me._endAngleDegMax - me._startAngleDeg))
      }
      
      if (currEnd >  me._endAngleDegMax){
        currEnd = me._endAngleDegMax;
      } 
  
      //Now set me._endAngleDeg
      me._endAngleDeg = currEnd;
    }    
    else {
      //Right now, this gauge is hardcoded to turn in a clockwise manner. 
      //  Ensure that the arc can turn in a clockwise direction to get to the end angles
      while (me._endAngleDeg < me._startAngleDeg){
        me._endAngleDeg = me._endAngleDeg + 360.0;
      }
      
      //Ensure that endAngleDeg falls within the range from startAngleDeg to endAngleDegMax
      while (me._endAngleDeg > me._endAngleDegMax){
        me._endAngleDegMax = me._endAngleDegMax + 360.0;
      }
    }
    me.redraw();
  };


});

Conversion from Browser D3 Visualization to Design Studio SDK

I have put together a comparison of the code while going through the walkthrough listed above.

Part 1: Simple Static Visualization to Configurable Arc in Design Studio with Properties

Part 2: to be continued...

Why do I care about D3?

This guide was written as an assignment for Oregon State's CS 290 course. I selected this topic because I am a business intelligence developer working with SAP's platforms including BOBJ (BusinessObjects). My team is fairly new within the company and we are quickly learning our limitations for graphical representations. I plan to leverage what I learned from researching and writing this in development of extensions for use with Design Studio. For those who are unfamiliar, Design Studio makes heavy use of CSS and JavaScript.

Image