Skip to Content
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.

4 Comments
You must be Logged on to comment or reply to a post.
  • 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.

      1. Do you mean you want to control the measures at runtime?
        It is possible also with the SAPUI5 charts.
      2. I do agree that the vizProperties are quite complex, but it doesn’t affect the end result.
      • Hi Shai Sinai

         

        1. In SAPUI5 also we can control the measures at runtime but we have to code the whole handling in the Controller to dynamically render the chart here in D3 based on data directly the library manages automatically which is easy that’s it. the effort of handling dynamically is reduced.
        2. Yes, vizProperties are quite complex, and using D3 it’s a little bit easy but for sure end result is the same.

        Thank you.

        Regards

        Love Arora.