Technical Articles
Extend SAPUI5 for Custom Visualization Controls – Interactive MultiLineGraph
Introduction
During the last week, I was working on a project where the majority of the features were around the analytics and graphs. I started exploring the SAPUI5 SDK and Documentation but couldn’t find any suitable chart according to the requirement to show a comparison of various Counties and their respective data having
-
X-axis: Years
-
Y-axis: Product Sales across different Countries.
I started exploring with D3.js, the amazing visualization library that helps you to get different perspectives having different visualizations on the basis of data. I have gone through various blogs but found a kind of workaround everyone has done using the HTML template but I wanted to use the control inside XML Views like other SAPUI5 library Controls to re-use the same across various views where I can control various things by properties and data binding. Here, The control also should be user interactive with a nice look and feel.
To achieve the above requirement, after exploration I got to know we can define/develop our own control in SAPUI5 and develop our own library by extending the D3.js Charts. In this blog, I will explain the step by step develop of SAPUI5 Custom Control – MultiLineGraph by extending the D3.js.
Environment:
- SAP Full Stack WebIDE
Step by Step Process:
-
Create a SAPUI5 Project from File menu >> New >> Project from Template.
-
Select the SAPUI5
-
Fill the Project Name, View Name with View Type: XML
-
Create a new folder under webapp with name lib >> customsdk.
- Create a File “library.js” under customsdk
sap.ui.define([ 'jquery.sap.global', 'sap/ui/core/library' ], function(jQuery, library) { "use-strict"; /** * Suite controls library. * * @namespace * @name customlib.graph * @author aroralove7@gmail.com * @version ${version} * @public */ var mylib={}; sap.ui.getCore().initLibrary({ name : "customlib.graph", noLibraryCSS: true, version: "${version}", dependencies : ["sap.ui.core", "sap.m"], types: [], interfaces: [], controls: [ "customlib.graph.MultiLineGraph" ], elements: [] }); return customlib.graph; }, /* bExport= */ false);
- Create a File “MultiLineGraphRenderer.js” under customsdk
sap.ui.define([], function() { "use strict"; /** * @namespace customlib.graph */ var MultiLineGraphRenderer = {}; /** * Renders the HTML for the control, using the provided {@link sap.ui.core.RenderManager}. * * @param {sap.ui.core.RenderManager} oRm RenderManager object * @param {sap.ui.core.Control} oControl An object representation of the control that will be rendered */ MultiLineGraphRenderer.render = function(oRm, oControl) { oRm.write("<div"); oRm.writeControlData(oControl); oRm.writeClasses(); oRm.writeStyles(); oRm.write(">"); }; return MultiLineGraphRenderer; }, /* bExport= */ true);
- Create a File “MultiLineGraph.js” under customsdk
/* * A simple UI5 control wrapping the HTML5 media API * allowing the library user to easily take Pictures in javascript * very easily. The control renders a Video preview element * (technically a video html tag). When clicked the image is grabbed * as a base64 encoded PNG. In the future would be nice to have the * format configurable. */ sap.ui.define([ 'jquery.sap.global', 'sap/ui/core/Control', 'com/sample/custom/graph/D3_Graphs_Exension/lib/customD3/core/d3.v4.min' ], function (jQuery, Control) { "use strict"; /** * Constructor for a new MultiLine Graph control. * * @param {string} [sId] id for the new control, generated automatically if no id is given * @param {object} [mSettings] initial settings for the new control * * @class * * @public * @alias customlib.graph.multilinegraph */ var graph = Control.extend("customlib.graph.MultiLineGraph", { /** * Control API */ metadata: { properties: { "id": { type: "string", defaultValue: "" }, "width": { type: "string", defaultValue: "640" }, /** * Height of the preview window in pixels */ "height": { type: "string", defaultValue: "480" } }, events: { } }, /** * Lifecycle hook to initialize the control */ init: function () { var that = this; this._data = {}; // Is the control displaying video at the moment? }, initializeData: function (oData) { this._data = oData; }, renderGraph: function (data) { var width = 500; var height = 300; var margin = 50; var duration = 250; var lineOpacity = "0.25"; var lineOpacityHover = "0.85"; var otherLinesOpacityHover = "0.1"; var lineStroke = "1.5px"; var lineStrokeHover = "2.5px"; var circleOpacity = '0.85'; var circleOpacityOnLineHover = "0.25" var circleRadius = 3; var circleRadiusHover = 6; /* Format Data */ var parseDate = d3.timeParse("%Y"); data.forEach(function (d) { d.values.forEach(function (d) { d.date = parseDate(d.date); d.value = +d.value; }); }); /* Scale */ var xScale = d3.scaleTime() .domain(d3.extent(data[0].values, d => d.date)) .range([0, width - margin]); var yScale = d3.scaleLinear() .domain([0, d3.max(data[0].values, d => d.value)]) .range([height - margin, 0]); var color = d3.scaleOrdinal(d3.schemeCategory10); /* Add SVG */ var svg = d3.select("#"+this.getId()).append("svg") .attr("width", (width + margin) + "px") .attr("height", (height + margin) + "px") .append('g') .attr("transform", `translate(${margin}, ${margin})`); /* Add line into SVG */ var line = d3.line() .x(d => xScale(d.date)) .y(d => yScale(d.value)); let lines = svg.append('g') .attr('class', 'lines'); lines.selectAll('.line-group') .data(data).enter() .append('g') .attr('class', 'line-group') .on("mouseover", function (d, i) { svg.append("text") .attr("class", "title-text") .style("fill", color(i)) .text(d.name) .attr("text-anchor", "middle") .attr("x", (width - margin) / 2) .attr("y", 5); }) .on("mouseout", function (d) { svg.select(".title-text").remove(); }) .append('path') .attr('class', 'line') .attr('d', d => line(d.values)) .style('stroke', (d, i) => color(i)) .style('opacity', lineOpacity) .on("mouseover", function (d) { d3.selectAll('.line') .style('opacity', otherLinesOpacityHover); d3.selectAll('.circle') .style('opacity', circleOpacityOnLineHover); d3.select(this) .style('opacity', lineOpacityHover) .style("stroke-width", lineStrokeHover) .style("cursor", "pointer"); }) .on("mouseout", function (d) { d3.selectAll(".line") .style('opacity', lineOpacity); d3.selectAll('.circle') .style('opacity', circleOpacity); d3.select(this) .style("stroke-width", lineStroke) .style("cursor", "none"); }); /* Add circles in the line */ lines.selectAll("circle-group") .data(data).enter() .append("g") .style("fill", (d, i) => color(i)) .selectAll("circle") .data(d => d.values).enter() .append("g") .attr("class", "circle") .on("mouseover", function (d) { d3.select(this) .style("cursor", "pointer") .append("text") .attr("class", "text") .text(`${d.value}`) .attr("x", d => xScale(d.date) + 5) .attr("y", d => yScale(d.value) - 10); }) .on("mouseout", function (d) { d3.select(this) .style("cursor", "none") .transition() .duration(duration) .selectAll(".text").remove(); }) .append("circle") .attr("cx", d => xScale(d.date)) .attr("cy", d => yScale(d.value)) .attr("r", circleRadius) .style('opacity', circleOpacity) .on("mouseover", function (d) { d3.select(this) .transition() .duration(duration) .attr("r", circleRadiusHover); }) .on("mouseout", function (d) { d3.select(this) .transition() .duration(duration) .attr("r", circleRadius); }); /* Add Axis into SVG */ var xAxis = d3.axisBottom(xScale).ticks(5); var yAxis = d3.axisLeft(yScale).ticks(5); svg.append("g") .attr("class", "x axis") .attr("transform", `translate(0, ${height - margin})`) .call(xAxis); svg.append("g") .attr("class", "y axis") .call(yAxis) .append('text') .attr("y", 15) .attr("transform", "rotate(-90)") .attr("fill", "#000") .text("Total values"); }, onAfterRendering: function () { var data = this._data; if (data.length > 0) { this.renderGraph(data); } else { this.renderGraphNoData(); } } }); return graph; });
- Now Load the library which we have developed for the application inside manifest.json under “sap.ui5”
"resourceRoots": { "customlib.graph":"./lib/customsdk" }
- In XML View
<mvc:View controllerName="com.sample.custom.graph.D3_Graphs_Exension.controller.RenderGraph" xmlns:graph="customlib.graph" xmlns:mvc="sap.ui.core.mvc" displayBlock="true" xmlns="sap.m"> <Shell id="shell"> <App id="app"> <pages> <Page id="page" title="Custom MultiLine Chart"> <content> <graph:MultiLineGraph id="mychart" /> </content> </Page> </pages> </App> </Shell> </mvc:View>
- In Controller
var data = [ { name: "India", values: [ {date: "2000", value: "100"}, {date: "2001", value: "110"}, {date: "2002", value: "145"}, {date: "2003", value: "241"}, {date: "2004", value: "101"}, {date: "2005", value: "90"}, {date: "2006", value: "10"}, {date: "2007", value: "35"}, {date: "2008", value: "21"}, {date: "2009", value: "201"} ] }, { name: "Germany", values: [ {date: "2000", value: "200"}, {date: "2001", value: "120"}, {date: "2002", value: "33"}, {date: "2003", value: "21"}, {date: "2004", value: "51"}, {date: "2005", value: "190"}, {date: "2006", value: "120"}, {date: "2007", value: "85"}, {date: "2008", value: "221"}, {date: "2009", value: "101"} ] }, { name: "USA", values: [ {date: "2000", value: "50"}, {date: "2001", value: "10"}, {date: "2002", value: "5"}, {date: "2003", value: "71"}, {date: "2004", value: "20"}, {date: "2005", value: "9"}, {date: "2006", value: "220"}, {date: "2007", value: "235"}, {date: "2008", value: "61"}, {date: "2009", value: "10"} ] } ]; this.getView().byId('mychart').initializeData(data);
- Add CSS classes for nice colors!
svg { font-family: Sans-Serif, Arial; } .line { stroke-width: 2; fill: none; } .axis path { stroke: black; } .text { font-size: 12px; } .title-text { font-size: 12px; }
- Now all configurations are done and we are ready!!
Result
Github
You can find the project setup and code here.
References
- D3: d3js.org
- SAP Full Stack WebIDE: https://account.hanatrial.ondemand.com/
SAPUI5 is a flexible framework to extend and develop the controls according to our needs. D3 is one of the best library for implementing graphs with best user experience.
I hope this blog will help you all to implement custom control by extending D3 charts as SAPUI5 control to provide best user experience with minimal efforts.
Happy Learning!
Love Arora.
Thanks for sharing.
D3.js is indeed a powerful library which might come in handy in many cases and the step-by-step is great.
May you please elaborate what were the advantages of this D3 Line chart over the standard SAPUI5 Line chart in your case?
Hi Shai Sinai,
The major advantages using the D3 Line chart is the flexibility, In SAPUI5 It’s bit complex where I have to define the “MeasureDefinitions” at design time and during runtime, I can only enable Measure Definitions which are defined already in vizChart but using D3 all these things we can manage easily and it’s flexible to handle the same with data supply itself.
Another major advantage of D3 is the easy management of properties compared to vizProperties we have to manage to SAPUI5 Line charts.
I hope this clarifies!
Thank you.
Regards
Love Arora.
Thanks for the response, but I'm not sure I've understood.
It is possible also with the SAPUI5 charts.
Hi Shai Sinai
Thank you.
Regards
Love Arora.