Technology Blogs by SAP
Learn how to extend and personalize SAP applications. Follow the SAP technology blog for insights into SAP BTP, ABAP, SAP Analytics Cloud, SAP HANA, and more.
cancel
Showing results for 
Search instead for 
Did you mean: 
Amiya
Employee
Employee
In this blog post I will be sharing how you can implement a D3 chart in SAP UI5 as custom control. Over the last couple of weeks I have been exploring D3.js, the JavaScript visualization library that allows you to programmatically create SVG visualizations on the basis of a data set.

D3 stands for Data Driven Documents, which creates graphical visualization based on the data. The requirement for our project was to create a tree structure where the parents may have many children with different properties, and we needed to represent their state in different colors as well.

There is a similar chart available on D3 gallery.


This is how it looks like.



 

For implementing this in any HTML page. You can use this code directly over your page. But I wanted to integrate it with our SAP UI5 page. I also wanted it to be reusable on different pages with dynamic binding. I wanted tool-tips and other color visualizations on this chart. Along with this, I needed it to be aligned with our UI5 user experience. So, I chose to make a Custom control out of this and change it as per our requirements.

There were a lot of things I’ve written while creating this control, but here I’m explaining only the basic part of that.

For our control these are the properties & methods

Properties:

Id : String

Methods:

setData(data) : to set data to be bonded with the chart

 
sap.ui.core.Control.extend('monitoring.TreeChart', {
metadata: {
properties: {
id: {
type: 'string',
defaultValue: ""
},
}
},
init: function () {
this.treeData = {};
},
setData: function (d) {
this.treeData = d;
},
renderer: function (oRm, oControl) {
oRm.write("<div");
oRm.writeControlData(oControl);
oRm.addClass("svgBoxBorder");
oRm.writeClasses();
oRm.write(">");
oRm.write("</div>");
},

noData: function () {
var text = d3v4.select("#" + this.getId())
.append('div')
.attr('class', 'noData')
.html("No Data");
},

createTree: function (treeData) {
// Set the dimensions and margins of the diagram
var margin = {
top: 20,
right: 90,
bottom: 30,
left: 150
},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;

// append the svg object to the body of the page
// appends a 'group' element to 'svg'
// moves the 'group' element to the top left margin
var svg = d3v4.select("#" + this.getId())
.append("svg")
.attr("width", width + margin.right + margin.left)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" +
margin.left + "," + margin.top + ")");

var i = 0,
duration = 750,
root;

// declares a tree layout and assigns the size
var treemap = d3v4.tree().size([height, width]);

// Assigns parent, children, height, depth
root = d3v4.hierarchy(treeData, function (d) {
return d.children;
});
root.x0 = height / 2;
root.y0 = 0;

// Collapse after the second level
root.children.forEach(collapse);

update(root);

// Collapse the node and all it's children
function collapse(d) {
if (d.children) {
d._children = d.children
d._children.forEach(collapse)
d.children = null
}
}

function update(source) {

// Assigns the x and y position for the nodes
var treeData = treemap(root);

// Compute the new tree layout.
var nodes = treeData.descendants(),
links = treeData.descendants().slice(1);

// Normalize for fixed-depth.
nodes.forEach(function (d) {
d.y = d.depth * 250
});

// ****************** Nodes section ***************************

// Update the nodes...
var node = svg.selectAll('g.node')
.data(nodes, function (d) {
return d.id || (d.id = ++i);
});

// Add Circle for the nodes
nodeEnter.append('circle')
.attr('class', 'node')
.attr('r', 1e-6)
.style("stroke", function (d) {
if (d.data.color == "GREEN")
return ("#61a656 ");
else if (d.data.color == "RED")
return ("#d32030 ");
else if (d.data.color == "AMBER")
return ("#e17b24");
});

// Add labels for the nodes
nodeEnter.append('text')
.attr("dy", ".35em")
.attr("x", function (d) {
return d.children || d._children ? -13 : 13;
})
.attr("text-anchor", function (d) {
return d.children || d._children ? "end" : "start";
})
.text(function (d) {
return d.data.DisplayName;
});

// UPDATE
var nodeUpdate = nodeEnter.merge(node);

// Transition to the proper position for the node
nodeUpdate.transition()
.duration(duration)
.attr("transform", function (d) {
return "translate(" + d.y + "," + d.x + ")";
});

// Update the node attributes and style
nodeUpdate.select('circle.node')
.attr('r', 3)
.attr('cursor', 'pointer')
.style("fill", function (d) {
if (d.data.color == "GREEN")
return ("#61a656 ");
else if (d.data.color == "RED")
return ("#d32030 ");
else if (d.data.color == "AMBER")
return ("#e17b24");
});

// Remove any exiting nodes
var nodeExit = node.exit().transition()
.duration(duration)
.attr("transform", function (d) {
return "translate(" + source.y + "," + source.x + ")";
})
.remove();

// On exit reduce the node circles size to 0
nodeExit.select('circle')
.attr('r', 1e-6);

// On exit reduce the opacity of text labels
nodeExit.select('text')
.style('fill-opacity', 1e-6);

// ****************** links section ***************************

// Update the links...
var link = svg.selectAll('path.link')
.data(links, function (d) {
return d.id;
});

// Enter any new links at the parent's previous position.
var linkEnter = link.enter().insert('path', "g")
.attr("class", "link")
.attr('d', function (d) {
var o = {
x: source.x0,
y: source.y0
}
return diagonal(o, o)
})
.style("stroke", function (d) {
if (d.data.color == "GREEN")
return ("#c2dcbe");
else if (d.data.color == "RED")
return ("#f8aaa0");
else if (d.data.color == "AMBER")
return ("#fce1a4");
});

// UPDATE
var linkUpdate = linkEnter.merge(link);

// Transition back to the parent element position
linkUpdate.transition()
.duration(duration)
.attr('d', function (d) {
return diagonal(d, d.parent)
});

// Remove any exiting links
var linkExit = link.exit().transition()
.duration(duration)
.attr('d', function (d) {
var o = {
x: source.x,
y: source.y
}
return diagonal(o, o)
})
.remove();

// Store the old positions for transition.
nodes.forEach(function (d) {
d.x0 = d.x;
d.y0 = d.y;
});

// Creates a curved (diagonal) path from parent to the child nodes
function diagonal(s, d) {
path = "M " + s.y + " " + s.x +
" C " + ((s.y + d.y) / 2) + " " + s.x + ", " +
((s.y + d.y) / 2) + " " + d.x + ", " +
d.y + " " + d.x;
return path
}

// Toggle children on click.
function click(d) {
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}

if (!(d.children || d._children)) {
sap.ui.controller("com.sap.monitoring.modules.topology.TopologyController").onDialogOpen(d);
}
update(d);
}

//On mouse hover on node show tooltip
function onMouseOver(d, e) {
var colorHtml = '<td class="tooltipValue greyText">Failed';
if (d.data.color == "GREEN")
colorHtml = '<td class="tooltipValue greenText">Ok';
else if (d.data.color == "RED")
colorHtml = '<td class="tooltipValue redText">Danger';
else if (d.data.color == "AMBER")
colorHtml = '<td class="tooltipValue amberText">Caution';
else if (d.data.color == "UNKNOWN")
colorHtml = '<td class="tooltipValue greyText">Failed';
else if (d.data.color == "DISABLED")
colorHtml = '<td class="tooltipValue greyText">Disabled';

var htmlText = '<table><tr><td class="tooltipLabel">Name:</td><td class="tooltipValue">' + d.data.DisplayName + '</td></tr>' +
'<tr><td class="tooltipLabel">Watch Name:</td><td class="tooltipValue">' + d.data.WatchName + '</td></tr>' +
'<tr><td class="tooltipLabel">State:</td>' + colorHtml + '</td></tr></table>';

var x = d.y;
var y = d.x;
if (y > 400) {
return d3v4.select(".svgBoxBorder")
.append("div")
.attr("class", "tooltipBox")
.style("top", (y - 120) + "px")
.style("left", (x + 80) + "px")
.html(htmlText)
.style("visibility", "visible");
}
return d3v4.select(".svgBoxBorder")
.append("div")
.attr("class", "tooltipBox")
.style("top", (y + 40) + "px")
.style("left", (x + 80) + "px")
.html(htmlText)
.style("visibility", "visible");
}

//On mouse out on node remove tooltip
function onMouseOut() {
d3v4.selectAll(".tooltipBox").remove();
}
}
},

onAfterRendering: function () {
var treeData = this.treeData;
if (treeData.children.length > 0) {
this.createTree(treeData);
} else {
this.noData();
}
}
});

Now while using this control you can can call the constructor just like any other UI5 control and set data to it by calling setData() function.


var oTreeChart = new monitoring.TreeChart({
id:"tree_chart"
});
oTreeChart.setData(treeData);

Or if you want to use it in XML View then you can use it like this and set data in the controller.


<TreeChart id="tree_chart"></TreeChart>

in controller


var oTreeChart=this.getView().byId("tree_chart");
oTreeChart.setData(treeData);

Here treeData is a JSON object containing the Display Name & status(color) of the nodes.

Finally this is how it looks like.



 

SAPUI5 is an excellent framework to quickly develop web applications & D3 is one of the best library for implementing data visualization by means of graphs. Here together they make both the development and user experience easier and smooth.

I hope this implementation of D3 as UI5 control will help you. Feel free to ask if you need any clarification while implementing it.

 
7 Comments