This article is for my fellow coders who would like to add Web charts component of D3 in SAPUI5 as custom controls.

When I started working on adding D3 component as a custom component in SAPUI5, I had little knowledge on SAPUI5 & D3 both. I kept searching net on the ways to integrate D3 with SAPUI5 but unfortunately found very less material on their association, all I could find was http://scn.sap.com/community/developer-center/front-end/blog/2014/07/17/custom-sapui5-visualization-controls-with-d3js which is a great article to follow!

After some knowledge on how custom components are created, I could get the spider chart into SAPUI5 and hence I wanted to share the step by step process of creating one.

So, let’s get straight to the point and start implementing web chart in SAPUI5.

A  little theory and preaching – Any component outside of SAPUI5 shall be adapted or created as a custom control in SAPUI5, basically we say that it’s a component which is customized accordingly in SAPUI5. Though there exist other approaches also but creating custom control is the logically correct way to implement.

There are two stages to it –

  1. Create the component
  2. Use the component

1. Create the component

To create the component, we need to do the following –

  1. Add the required library for the new componentDefine the base – Any tree knows where it is rooted from, a component shall know what it’s base is, where it is sprouted from. In our case, it is D3 library, so let’s add it like this –

jQuery.sap.require(“sap/ui/thirdparty/d3”);


  b. Declare the componentWho am I?

How would the component like to be declared as? Your complete name consists of first name and the last name, that’s  your fully qualified name. same way, whenever someone calls you, you know, you are called. Same way, we need to declare what is the name of this component? The fully qualified name like this –

  jQuery.sap.declare(“<companyname>.<groupname>.<productname>.customcontrols.RadarChart”);


c. Define the structure

                 Add metadata/properties

Define what your component will have? Here we have an axis and the value, that is all we will need to render the web chart, each line in the chart is it’s axis and the point plotted on the axis represents it’s value. This definition is done as follows –

sap.ui.core.Control.extend(“<companyname>.<groupname>.<productname>.RadarChart”, {

       metadata : {

              properties: {

                     “axis” : {type : “string”, group : “Misc”, defaultValue : null},

                     “value” : {type : “string”, group : “Misc”, defaultValue : null}

              }

       },



Both axis and value are defined as strings. We don’t need any default value to be set and hence we initialize it to null. Group is defined as miscellaneous.

d. Define init –

Init tells which code needs to be executed right after the component is initialized. Here, we we are not doing much, just setting the parentID to blank (this will later be set).

   

   init : function() {

              console.log(“Getting ready to weave a web..”);

              this.sParentId = “”;

       },


e. Finally Render – this is where all the action takes place, placing the complete code of D3 radar chart in this method. This method is used to render the custom control.

renderer : function(oRm, oControl) {

              var layout = oControl.createRadars();

              oRm.write(“<div”);

              oRm.writeControlData(layout); // writes the Control ID and enables event handling – important!

              oRm.writeClasses(); // there is no class to write, but this enables

              // support for ColorBoxContainer.addStyleClass(…)          

              oRm.write(“>”);

              oRm.renderControl(layout);

              oRm.addClass(‘verticalAlignment’);

              oRm.write(“</div>”);

       },

  

       onAfterRendering: function(){

              var d = [];

              d = this.getModel().oData;

                var cfg = {

                                   radius: 5,

                                   w: 600,

                                   h: 600,

                                   factor: 1,

                                   factorLegend: .85,

                                   levels: 3,

                                   maxValue: 0,

                                   radians: 2 * Math.PI,

                                   opacityArea: 0.5,

                                   ToRight: 5,

                                   TranslateX: 80,

                                   TranslateY: 30,

                                   ExtraWidthX: 100,

                                   ExtraWidthY: 100,

                                   color: d3.scale.category10()

                                  };

         

                var w = 500,

                     h = 500;

                var options = {

                             w: w,

                             h: h,

                             maxValue: 0.6,

                             levels: 6,

                             ExtraWidthX: 300

                           };

           

                var vis = d3.select(“#” + this.sParentId);

         

              if(‘undefined’ !== typeof options){

                       for(var i in options){

                           if(‘undefined’ !== typeof options[i]){

                             cfg[i] = options[i];

                           }

                       }

                     }

                     cfg.maxValue = Math.max(cfg.maxValue, d3.max(d, function(i){return d3.max(i.map(function(o){return o.value;}))}));

                     var allAxis = (d[0].map(function(i, j){return i.axis}));

                     var total = allAxis.length;

                     var radius = cfg.factor*Math.min(cfg.w/2, cfg.h/2);

                     var Format = d3.format(‘%’);

                     //d3.svg.remove();//d3.select(vis).select(“svg“).remove();

                

                     var g = //d3.select(vis)

                                  vis

                                  .append(“svg”)

                                  .attr(“width”, cfg.w+cfg.ExtraWidthX)

                                  .attr(“height”, cfg.h+cfg.ExtraWidthY)

                                  .append(“g”)

                                  .attr(“transform”, “translate(“ + cfg.TranslateX + “,” + cfg.TranslateY + “)”);

                                  ;

                     var tooltip;

                

                     //Circular segments

                     for(var j=0; j<cfg.levels-1; j++){

                       var levelFactor = cfg.factor*radius*((j+1)/cfg.levels);

                       g.selectAll(“.levels”)

                        .data(allAxis)

                        .enter()

                        .append(“svg:line”)

                        .attr(“x1”, function(d, i){return levelFactor*(1-cfg.factor*Math.sin(i*cfg.radians/total));})

                        .attr(“y1”, function(d, i){return levelFactor*(1-cfg.factor*Math.cos(i*cfg.radians/total));})

                        .attr(“x2”, function(d, i){return levelFactor*(1-cfg.factor*Math.sin((i+1)*cfg.radians/total));})

                        .attr(“y2”, function(d, i){return levelFactor*(1-cfg.factor*Math.cos((i+1)*cfg.radians/total));})

                        .attr(“class”, “line”)

                        .style(“stroke”, “grey”)

                        .style(“stroke-opacity”, “0.75”)

                        .style(“stroke-width”, “0.3px”)

                        .attr(“transform”, “translate(“ + (cfg.w/2-levelFactor) + “, “ + (cfg.h/2-levelFactor) + “)”);

                     }

                     //Text indicating at what % each level is

                     for(var j=0; j<cfg.levels; j++){

                       var levelFactor = cfg.factor*radius*((j+1)/cfg.levels);

                       g.selectAll(“.levels”)

                        .data([1]) //dummy data

                        .enter()

                        .append(“svg:text”)

                        .attr(“x”, function(d){return levelFactor*(1-cfg.factor*Math.sin(0));})

                        .attr(“y”, function(d){return levelFactor*(1-cfg.factor*Math.cos(0));})

                        .attr(“class”, “legend”)

                        .style(“font-family”, “sans-serif”)

                        .style(“font-size”, “10px”)

                        .attr(“transform”, “translate(“ + (cfg.w/2-levelFactor + cfg.ToRight) + “, “ + (cfg.h/2-levelFactor) + “)”)

                        .attr(“fill”, “#737373”)

.text(Format((j+1)*cfg.maxValue/cfg.levels));

                     }

                

                     series = 0;

                     var axis = g.selectAll(“.axis”)

                                  .data(allAxis)

                                  .enter()

                                  .append(“g”)

                                  .attr(“class”, “axis”);

                     axis.append(“line”)

                           .attr(“x1”, cfg.w/2)

                           .attr(“y1”, cfg.h/2)

                           .attr(“x2”, function(d, i){return cfg.w/2*(1-cfg.factor*Math.sin(i*cfg.radians/total));})

                           .attr(“y2”, function(d, i){return cfg.h/2*(1-cfg.factor*Math.cos(i*cfg.radians/total));})

                           .attr(“class”, “line”)

                           .style(“stroke”, “grey”)

                           .style(“stroke-width”, “1px”);

                     axis.append(“text”)

                           .attr(“class”, “legend”)

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

                           .style(“font-family”, “sans-serif”)

                           .style(“font-size”, “11px”)

                           .attr(“text-anchor”, “middle”)

                           .attr(“dy”, “1.5em”)

                           .attr(“transform”, function(d, i){return “translate(0, -10)”})

                           .attr(“x”, function(d, i){return cfg.w/2*(1-cfg.factorLegend*Math.sin(i*cfg.radians/total))-60*Math.sin(i*cfg.radians/total);})

                           .attr(“y”, function(d, i){return cfg.h/2*(1-Math.cos(i*cfg.radians/total))-20*Math.cos(i*cfg.radians/total);});

          

                     d.forEach(function(y, x){

                       dataValues = [];

                       g.selectAll(“.nodes”)

                           .data(y, function(j, i){

                             dataValues.push([

                                  cfg.w/2*(1-(parseFloat(Math.max(j.value, 0))/cfg.maxValue)*cfg.factor*Math.sin(i*cfg.radians/total)),

                                  cfg.h/2*(1-(parseFloat(Math.max(j.value, 0))/cfg.maxValue)*cfg.factor*Math.cos(i*cfg.radians/total))

                             ]);

                           });

                       dataValues.push(dataValues[0]);

                       g.selectAll(“.area”)

                                                 .data([dataValues])

                                                 .enter()

                                                 .append(“polygon”)

                                                 .attr(“class”, “radar-chart-serie”+series)

                                                 .style(“stroke-width”, “2px”)

                                                 .style(“stroke”, cfg.color(series))

                                                 .attr(“points”,function(d) {

                                                        var str=“”;

                                                        for(var pti=0;pti<d.length;pti++){

                                                               str=str+d[pti][0]+“,”+d[pti][1]+” “;

                                                        }

                                                        return str;

                                                  })

                                                 .style(“fill”, function(j, i){return cfg.color(series)})

                                                 .style(“fill-opacity”, cfg.opacityArea)

                                                 .on(‘mouseover’, function (d){

                                                                                  z = “polygon.”+d3.select(this).attr(“class”);

                                                                                  g.selectAll(“polygon”)

                                                                                   .transition(200)

                                                                                   .style(“fill-opacity”, 0.1);

                                                                                  g.selectAll(z)

                                                                                   .transition(200)

                                                                                   .style(“fill-opacity”, .7);

                                                                             })

                                                 .on(‘mouseout’, function(){

                                                                                  g.selectAll(“polygon”)

                                                                                   .transition(200)

                                                                                   .style(“fill-opacity”, cfg.opacityArea);

                                                 });

                       series++;

                     });

                     series=0;

                     d.forEach(function(y, x){

                       g.selectAll(“.nodes”)

                           .data(y).enter()

                           .append(“svg:circle”)

                           .attr(“class”, “radar-chart-serie”+series)

                           .attr(‘r’, cfg.radius)

                           .attr(“alt”, function(j){return Math.max(j.value, 0)})

                           .attr(“cx”, function(j, i){

                             dataValues.push([

                                  cfg.w/2*(1-(parseFloat(Math.max(j.value, 0))/cfg.maxValue)*cfg.factor*Math.sin(i*cfg.radians/total)),

                                  cfg.h/2*(1-(parseFloat(Math.max(j.value, 0))/cfg.maxValue)*cfg.factor*Math.cos(i*cfg.radians/total))

                           ]);

                           return cfg.w/2*(1-(Math.max(j.value, 0)/cfg.maxValue)*cfg.factor*Math.sin(i*cfg.radians/total));

                           })

                           .attr(“cy”, function(j, i){

                             return cfg.h/2*(1-(Math.max(j.value, 0)/cfg.maxValue)*cfg.factor*Math.cos(i*cfg.radians/total));

                           })

                           .attr(“data-id”, function(j){return j.axis})

                           .style(“fill”, cfg.color(series)).style(“fill-opacity”, .9)

                           .on(‘mouseover’, function (d){

                                                newX =  parseFloat(d3.select(this).attr(‘cx’)) – 10;

                                                newY =  parseFloat(d3.select(this).attr(‘cy’)) – 5;

                                           

                                                tooltip

                                                       .attr(‘x’, newX)

                                                       .attr(‘y’, newY)

                                                       .text(Format(d.value))

                                                       .transition(200)

                                                       .style(‘opacity’, 1);

                                                  

                                                z = “polygon.”+d3.select(this).attr(“class”);

                                                g.selectAll(“polygon”)

                                                       .transition(200)

                                                       .style(“fill-opacity”, 0.1);

                                                g.selectAll(z)

                                                       .transition(200)

                                                       .style(“fill-opacity”, .7);

                                           })

                           .on(‘mouseout’, function(){

                                                tooltip

                                                       .transition(200)

                                                       .style(‘opacity’, 0);

                                                g.selectAll(“polygon”)

                                                       .transition(200)

                                                       .style(“fill-opacity”, cfg.opacityArea);

                                           })

                           .append(“svg:title”)

                           .text(function(j){return Math.max(j.value, 0)});

                       series++;

                     });

                     //Tooltip

                     tooltip = g.append(‘text’)

                                     .style(‘opacity’, 0)

                                     .style(‘font-family’, ‘sans-serif’)

                                     .style(‘font-size’, ’13px’);

       }

e. Combining all the pieces together – this is how the complete code will look like –

jQuery.sap.require(“sap/ui/thirdparty/d3”);

jQuery.sap.declare(“bslsap.coil.ro.customcontrols.RadarChart”);

sap.ui.core.Control.extend(“<companyname>.<groupname>.<productname>.customcontrols.RadarChart”, {

  metadata : {

  properties: {

  “axis” : {type : “string”, group : “Misc”, defaultValue : null},

  “value” : {type : “string”, group : “Misc”, defaultValue : null}

  }

  },

  init : function() {

  console.log(“<companyname>.<groupname>.<productname>.customcontrols.RadarChart.init()”);

  this.sParentId = “”;

  },

  /**

  * The renderer render calls all the functions which are necessary to create the control,

  * then it call the renderer of the vertical layout

  * @param oRm {RenderManager}

  * @param oControl {Control}

  */

  renderer : function(oRm, oControl) {

  var layout = oControl.createRadars();

  oRm.write(“<div”);

  oRm.writeControlData(layout); // writes the Control ID and enables event handling – important!

  oRm.writeClasses(); // there is no class to write, but this enables

  // support for ColorBoxContainer.addStyleClass(…)

  oRm.write(“>”);

  oRm.renderControl(layout);

  oRm.addClass(‘verticalAlignment’);

  oRm.write(“</div>”);

  },

  onAfterRendering: function(){

  var d = [];

  d = this.getModel().oData;

   var cfg = {

  radius: 5,

  w: 600,

  h: 600,

  factor: 1,

  factorLegend: .85,

  levels: 3,

  maxValue: 0,

  radians: 2 * Math.PI,

  opacityArea: 0.5,

  ToRight: 5,

  TranslateX: 80,

  TranslateY: 30,

  ExtraWidthX: 100,

  ExtraWidthY: 100,

  color: d3.scale.category10()

  };

   var w = 500,

  h = 500;

   var options = {

   w: w,

   h: h,

   maxValue: 0.6,

   levels: 6,

   ExtraWidthX: 300

  };

   var vis = d3.select(“#” + this.sParentId);

  if(‘undefined’ !== typeof options){

   for(var i in options){

  if(‘undefined’ !== typeof options[i]){

   cfg[i] = options[i];

  }

   }

  }

  cfg.maxValue = Math.max(cfg.maxValue, d3.max(d, function(i){return d3.max(i.map(function(o){return o.value;}))}));

  var allAxis = (d[0].map(function(i, j){return i.axis}));

  var total = allAxis.length;

  var radius = cfg.factor*Math.min(cfg.w/2, cfg.h/2);

  var Format = d3.format(‘%’);

  //d3.svg.remove();//d3.select(vis).select(“svg”).remove();

  var g = //d3.select(vis)

  vis

  .append(“svg”)

  .attr(“width”, cfg.w+cfg.ExtraWidthX)

  .attr(“height”, cfg.h+cfg.ExtraWidthY)

  .append(“g”)

  .attr(“transform”, “translate(” + cfg.TranslateX + “,” + cfg.TranslateY + “)”);

  ;

  var tooltip;

  //Circular segments

  for(var j=0; j<cfg.levels-1; j++){

   var levelFactor = cfg.factor*radius*((j+1)/cfg.levels);

   g.selectAll(“.levels”)

    .data(allAxis)

    .enter()

    .append(“svg:line”)

    .attr(“x1”, function(d, i){return levelFactor*(1-cfg.factor*Math.sin(i*cfg.radians/total));})

    .attr(“y1”, function(d, i){return levelFactor*(1-cfg.factor*Math.cos(i*cfg.radians/total));})

    .attr(“x2”, function(d, i){return levelFactor*(1-cfg.factor*Math.sin((i+1)*cfg.radians/total));})

    .attr(“y2”, function(d, i){return levelFactor*(1-cfg.factor*Math.cos((i+1)*cfg.radians/total));})

    .attr(“class”, “line”)

    .style(“stroke”, “grey”)

    .style(“stroke-opacity”, “0.75”)

    .style(“stroke-width”, “0.3px”)

    .attr(“transform”, “translate(” + (cfg.w/2-levelFactor) + “, ” + (cfg.h/2-levelFactor) + “)”);

  }

  //Text indicating at what % each level is

  for(var j=0; j<cfg.levels; j++){

   var levelFactor = cfg.factor*radius*((j+1)/cfg.levels);

   g.selectAll(“.levels”)

    .data([1]) //dummy data

    .enter()

    .append(“svg:text”)

    .attr(“x”, function(d){return levelFactor*(1-cfg.factor*Math.sin(0));})

    .attr(“y”, function(d){return levelFactor*(1-cfg.factor*Math.cos(0));})

    .attr(“class”, “legend”)

    .style(“font-family”, “sans-serif”)

    .style(“font-size”, “10px”)

    .attr(“transform”, “translate(” + (cfg.w/2-levelFactor + cfg.ToRight) + “, ” + (cfg.h/2-levelFactor) + “)”)

    .attr(“fill”, “#737373”)

    .text(Format((j+1)*cfg.maxValue/cfg.levels));

  }

  series = 0;

  var axis = g.selectAll(“.axis”)

  .data(allAxis)

  .enter()

  .append(“g”)

  .attr(“class”, “axis”);

  axis.append(“line”)

  .attr(“x1”, cfg.w/2)

  .attr(“y1”, cfg.h/2)

  .attr(“x2”, function(d, i){return cfg.w/2*(1-cfg.factor*Math.sin(i*cfg.radians/total));})

  .attr(“y2”, function(d, i){return cfg.h/2*(1-cfg.factor*Math.cos(i*cfg.radians/total));})

  .attr(“class”, “line”)

  .style(“stroke”, “grey”)

  .style(“stroke-width”, “1px”);

  axis.append(“text”)

  .attr(“class”, “legend”)

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

  .style(“font-family”, “sans-serif”)

  .style(“font-size”, “11px”)

  .attr(“text-anchor”, “middle”)

  .attr(“dy”, “1.5em”)

  .attr(“transform”, function(d, i){return “translate(0, -10)”})

  .attr(“x”, function(d, i){return cfg.w/2*(1-cfg.factorLegend*Math.sin(i*cfg.radians/total))-60*Math.sin(i*cfg.radians/total);})

  .attr(“y”, function(d, i){return cfg.h/2*(1-Math.cos(i*cfg.radians/total))-20*Math.cos(i*cfg.radians/total);});

  d.forEach(function(y, x){

   dataValues = [];

   g.selectAll(“.nodes”)

  .data(y, function(j, i){

   dataValues.push([

  cfg.w/2*(1-(parseFloat(Math.max(j.value, 0))/cfg.maxValue)*cfg.factor*Math.sin(i*cfg.radians/total)),

  cfg.h/2*(1-(parseFloat(Math.max(j.value, 0))/cfg.maxValue)*cfg.factor*Math.cos(i*cfg.radians/total))

   ]);

  });

   dataValues.push(dataValues[0]);

   g.selectAll(“.area”)

  .data([dataValues])

  .enter()

  .append(“polygon”)

  .attr(“class”, “radar-chart-serie”+series)

  .style(“stroke-width”, “2px”)

  .style(“stroke”, cfg.color(series))

  .attr(“points”,function(d) {

  var str=””;

  for(var pti=0;pti<d.length;pti++){

  str=str+d[pti][0]+”,”+d[pti][1]+” “;

  }

  return str;

   })

  .style(“fill”, function(j, i){return cfg.color(series)})

  .style(“fill-opacity”, cfg.opacityArea)

  .on(‘mouseover’, function (d){

  z = “polygon.”+d3.select(this).attr(“class”);

  g.selectAll(“polygon”)

  .transition(200)

  .style(“fill-opacity”, 0.1);

  g.selectAll(z)

  .transition(200)

  .style(“fill-opacity”, .7);

   })

  .on(‘mouseout’, function(){

  g.selectAll(“polygon”)

  .transition(200)

  .style(“fill-opacity”, cfg.opacityArea);

  });

   series++;

  });

  series=0;

  d.forEach(function(y, x){

   g.selectAll(“.nodes”)

  .data(y).enter()

  .append(“svg:circle”)

  .attr(“class”, “radar-chart-serie”+series)

  .attr(‘r’, cfg.radius)

  .attr(“alt”, function(j){return Math.max(j.value, 0)})

  .attr(“cx”, function(j, i){

   dataValues.push([

  cfg.w/2*(1-(parseFloat(Math.max(j.value, 0))/cfg.maxValue)*cfg.factor*Math.sin(i*cfg.radians/total)),

  cfg.h/2*(1-(parseFloat(Math.max(j.value, 0))/cfg.maxValue)*cfg.factor*Math.cos(i*cfg.radians/total))

  ]);

  return cfg.w/2*(1-(Math.max(j.value, 0)/cfg.maxValue)*cfg.factor*Math.sin(i*cfg.radians/total));

  })

  .attr(“cy”, function(j, i){

   return cfg.h/2*(1-(Math.max(j.value, 0)/cfg.maxValue)*cfg.factor*Math.cos(i*cfg.radians/total));

  })

  .attr(“data-id”, function(j){return j.axis})

  .style(“fill”, cfg.color(series)).style(“fill-opacity”, .9)

  .on(‘mouseover’, function (d){

  newX =  parseFloat(d3.select(this).attr(‘cx’)) – 10;

  newY =  parseFloat(d3.select(this).attr(‘cy’)) – 5;

  tooltip

  .attr(‘x’, newX)

  .attr(‘y’, newY)

  .text(Format(d.value))

  .transition(200)

  .style(‘opacity’, 1);

  z = “polygon.”+d3.select(this).attr(“class”);

  g.selectAll(“polygon”)

  .transition(200)

  .style(“fill-opacity”, 0.1);

  g.selectAll(z)

  .transition(200)

  .style(“fill-opacity”, .7);

   })

  .on(‘mouseout’, function(){

  tooltip

  .transition(200)

  .style(‘opacity’, 0);

  g.selectAll(“polygon”)

  .transition(200)

  .style(“fill-opacity”, cfg.opacityArea);

   })

  .append(“svg:title”)

  .text(function(j){return Math.max(j.value, 0)});

   series++;

  });

  //Tooltip

  tooltip = g.append(‘text’)

    .style(‘opacity’, 0)

    .style(‘font-family’, ‘sans-serif’)

    .style(‘font-size’, ’13px’);

  }

});

2. Use the component –


To use the component we got to do following –

  1. Create a placeholder
  2. Create spider/radar chart
  3. Add spider to the holder
  4. Add data

1. Create a placeholder –

Add a holder, here I have taken FlexBox as a container to add the custom component like this –

           var ccRadarHolder = new sap.m.FlexBox(“radarHolder”, {“alignItems”: “Start”, “justifyContent”: “Center”});

2. Create spider chart by the following command –

var oRadar = new <companyname>.<groupname>.<productname>.customcontrols.RadarChart({axis:”{axis}”, value:”{value}”});

3. Add spider to the holder like this –

oRadarHolder.addItem(oRadar);

4. Add Data – The best thing about the D3 chart is seamless data integration using JSON which we will do now like this –  Note the axis and the value we defined during the creation in our spider chart initially.

var oModel = new sap.ui.model.json.JSONModel();

oModel.setData(

   [

    [

  {axis:”Facebook”,value:2.5},

  {axis:”Twitting”,value:2},

  {axis:”Shopping”,value:1},

  {axis:”Normal surfing”,value:1.5},

  {axis:”Chatting”,value:1.5},

   ]

    ,

   [

  {axis:”Study”,value:1},

  {axis:”Outdoor games”,value:1.5},

  {axis:”Gossiping”,value:.5},

   ]

  ]

  );

oRadar.setModel(oModel);

And we are done, following is the result –

/wp-content/uploads/2016/08/spider_1015544.png


So now you see you are really caught in the web, lessen those chatting surfing hours!! Catch the spider on your SAPUI5 now..go ahead!! 🙂

To report this post you need to login first.

Be the first to leave a comment

You must be Logged on to comment or reply to a post.

Leave a Reply