Custom SAPUI5 Visualization Controls with D3.js
Over the last couple of months I have been playing with D3.js, the JavaScript visualization library that allows you to programmatically create SVG visualizations on the basis of a data set. I love how this allows me to think up how to visualize a particular dataset without any constraints of the capabilities of any charts and graph package included in whatever tool you would otherwise have used. I have since built out custom extensions in Lumira using D3.js, and when I started looking at SAPUI5, one of the first things I wanted to do was create my own D3.js SAPUI5 custom controls.
There have been other articles on including D3.js in SAPUI5 (most notably here and here), but they used an HTML template, rather than include it in a custom control that could be used largely like any other SAPUI5 control. Well, after a bit of puzzling and some help from a colleague who had built custom controls based on other SAPUI5 controls for a different project, I managed to figure it out, and through this blog, I hope to help you create your own.
This is one of the controls I created. You see it here included in a page, embedded within a <Panel> tag. It receives a data set of target, currently booked revenue and a forecast, and then shows the percentage of booked revenue and forecast against the target. We will focus here largely on the SAPUI5 custom control parts rather than the D3.js code, but the control is available for download at the bottom of this blog.
Building the Chart Control
Let’s create our custom control: create a new folder called “controls” inside the WebContent folder of your SAPUI5 application, and create a new JavaScript file. We’ll use the attached D3ChartTemplate.js as an example. In your own controls you should change the class names (and filename) to something a bit more descriptive so you know which is which, and typically you’d call your JavaScript file the same as the control (i.e. BarChart.js would contain BarChart and BarChartItem classes).
We start with a requirement to load the built-in third party D3 library and a declaration of the chart itself:
jQuery.sap.require(“sap/ui/thirdparty/d3”);
jQuery.sap.declare(“sap.jaysdk.Chart”);
Then, we need to define our ChartItem. This is used to map between what the chart expects and the dataset you are trying to visualize. You’ll see this once we get to instantiating the chart.
sap.ui.core.Element.extend(“sap.jaysdk.ChartItem”, { metadata : {
properties : {
“region” : {type : “string”, group : “Misc”, defaultValue : null},
“budget” : {type : “string”, group : “Misc”, defaultValue : null},
“bw” : {type : “string”, group : “Misc”, defaultValue : null},
“forecast” : {type : “string”, group : “Misc”, defaultValue : null}
}
}});
We then set up the definition of the Chart itself:
sap.ui.core.Control.extend(“sap.jaysdk.Chart”, {
metadata : {
properties: {
“title”: {type : “string”, group : “Misc”, defaultValue : “Chart Title”}
},
aggregations : {
“items” : { type: “sap.jaysdk.ChartItem”, multiple : true, singularName : “item”}
}
,
defaultAggregation : “items”,
events: {
“onPress” : {},
“onChange”:{}
}
},
…
}
You see here the ChartItem referenced. Once data is passed in, these will become our “items”.
We need to split the setup of the chart separately from the D3.js code, since we need to create the placeholder first, while the D3.js code will only run after rendering the HTML through the Chart’s onAfterRendering() function. So, let’s create the chart:
createChart : function() {
/*
* Called from renderer
*/
console.log(“sap.jaysdk.Chart.createChart()”);
var oChartLayout = new sap.m.VBox({alignItems:sap.m.FlexAlignItems.Center,justifyContent:sap.m.FlexJustifyContent.Center});
var oChartFlexBox = new sap.m.FlexBox({height:“180px”,alignItems:sap.m.FlexAlignItems.Center});
/* ATTENTION: Important
* This is where the magic happens: we need a handle for our SVG to attach to. We can get this using .getIdForLabel()
* Check this in the ‘Elements’ section of the Chrome Devtools:
* By creating the layout and the Flexbox, we create elements specific for this control, and SAPUI5 takes care of
* ID naming. With this ID, we can append an SVG tag inside the FlexBox
*/
this.sParentId=oChartFlexBox.getIdForLabel();
oChartLayout.addItem(oChartFlexBox);
return oChartLayout;
},
We create a VBox and place a FlexBox inside of it. Then, as the comments already suggest, we get the magic. Rather than using a <div> tag or <span> tag that we write deliberately in an HTML file, we’re going to leverage the identifier of the FlexBox to attach our SVG to. This is what it looks like in the Chrome DevTools “Elements” section:
The “__box2” id is what we will use and is stored in this.sParentId.
We have a pretty standard renderer function, with the only difference that we call the createChart function.
renderer : function(oRm, oControl) {
var layout = oControl.createChart();
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>”);
},
Finally, the Chart is completed in the onAfterRendering() function:
onAfterRendering: function(){
console.log(“sap.jaysdk.Chart.onAfterRendering()”);
console.log(this.sParentId);
var cItems = this.getItems();
var data = [];
for (var i=0;i<cItems.length;i++){
var oEntry = {};
for (var j in cItems[i].mProperties) {
oEntry[j]=cItems[i].mProperties[j];
}
data.push(oEntry);
}
//console.log(“Data:”);
//console.log(data);
/*
* ATTENTION: See .createChart()
* Here we’re picking up a handle to the “parent” FlexBox with the ID we got in .createChart()
* Now simply .append SVG elements as desired
* EVERYTHING BELOW THIS IS PURE D3.js
*/
var vis = d3.select(“#” + this.sParentId);
//Your D3.js code HERE
}
We get the “items” that were passed in containing our data, and read it into the data array. Once that has run, you have the dataset from your model in the chart and you are ready to build out your SVG. We can now attach to the <div> tag with the id we retrieved before using d3.select. After that, it is just standard D3.js code!
Using the Chart control
We can now try to use the chart in our SAPUI5 pages. We start with creating a container inside the Xxx.view.xml file:
<Panel>
<headerToolbar>
<Toolbar>
<Label text=“YTD Booked/Won and Forecast to Budget (Percentage)” />
</Toolbar>
</headerToolbar>
<FlexBox id=“ChartHolder” alignItems=“Start” justifyContent=“Center”>
</FlexBox>
</Panel>
This creates the location that we will place our chart in. Now we need to add the chart to our Xxx.controller.js. We need start with declaring the Chart and the ChartItem. The resource path should be the path to your custom control relative to index.html and its name without the js extension. Notice that since the ChartItem is embedded in the controls/Chart.js file, its module path is controls/Chart. Again, for your own controls, use the same convention, but obviously a more descriptive name for your custom chart.
jQuery.sap.registerModulePath(“sap.jaysdk.Chart”, “controls/Chart”);
jQuery.sap.require(“sap.jaysdk.Chart”);
jQuery.sap.registerModulePath(“sap.jaysdk.ChartItem”, “controls/Chart”);
jQuery.sap.require(“sap.jaysdk.ChartItem”);
Next, we instantiate our chart, inside the onBeforeRendering() function (or a separate function call from within that event function). We find the ChartHolder FlexBox, create a ChartItem that maps the fields from the data in your model to the chart, and create a new chart (here with a path included). We then get a handle on the model and set the model to the chart. Finally, we add the chart to the FlexBox container.
var oChartHolder = this.byId(“ChartHolder”);
var oChartItem = new sap.jaysdk.ChartItem({region:“{region}”, budget:“{budget}”, bw:“{bw_ytd}”, forecast:“{forecast}”});
/* new chart */
var oChart = new sap.jaysdk.Chart({
items: {path : “/regions”, template : oChartItem}
});
var oModel = sap.ui.getCore().getModel(“chart”);
oChart.setModel(oModel);
oChartHolder.addItem(oChart);
And that’s it! Your chart should now be visible in your application, and can be embedded like any other control.
Attached below you’ll find the D3ChartTemplate as well as the PerformanceToTargetComparison chart of which a screenshot is shown at the top of this blog. I have also included a JSON file with the sample data.
SAPUI5 is an excellent framework to quickly develop business applications, and using the techniques described here, you can now add any fancy visualization you would like. Happy hacking!
In the view controller that adds the Custom D3Control, I suggest to use onAfterRendering() hook.
Because when I use the onBeforeRendering() hook in the view controller to add the Custom D3Chart, I got two charts.
When I turn to use onAfterRendering() hook to add it, the issue solved - only one chart.
I guess that's because I test the view that uses the Custom D3Chart as the first screen of my simple HTML app, SAPUI5 fires firstly the view's onInit() hook then fires the view's onBeforeRendering() hook, so I got twice onBeforeRendering() and two Custom D3Chart.
Hi Zhunzhun,
Thanks for that. I think you're right, that it depends a bit on the structure of your application when and how the events fire and where to put the initialization. I didn't see this problem with my own charts using this approach, but I did see it in a previous attempt where I was passing in different data to the same page, and at each new access I would get a new chart added.
I actually got around that by simply calling .selectAll("svg").remove(); before adding any new content.
I deliberately put console.log messages at the start of my functions to show which functions are called. If onAfterRendering() fixes it, let's just use that one, then.
Thanks again!
Hi Jay,
Thanks for your reply, it make me more clear.
How about put the "add Custom D3Chart to oChartHolder " code in the onInit() hook and "reset the data binding" code in onBeforeShow() hook?
As "add Custom D3Chart to oChartHolder " is one-time initialization and "a setting that are not possible in the XMLView, it can be set in the controller's onInit() hook instead". (quoted by Andreas Kunz)
But "reset the data binding" code can reuse the Custom D3Chart setted in onInit() hooks for multiple times, then we use the onBeforeRendering() hook to reset the model before rerendering.
_______________________________
some more question here, the Custom D3Chart seems not re-rendering when I reset the Data Bind in Google Chrome console:
sap.ui.getCore().byId("__xmlview0--ChartHolder").setModel(new sap.ui.model.json.JSONModel({}))
or
var data = sap.ui.getCore().byId("__xmlview0--ChartHolder").getModel().getData();
data.regions[0].region = "China";
sap.ui.getCore().byId("__xmlview0--ChartHolder").getModel().setData(data)
Is there something additional to do in Costom D3Chart?
Thanks,
Antoine
Thanks, Zhunzhun. Great stuff.
As far as Antoine's question goes: I would have assumed you'd set the model again, rather than just set new data to the model.
But I've struggled with models, data and updates myself, so not sure what is going on. The SVG gets built out in onAfterRendering() so perhaps doing it this way doesn't re-render the control? Maybe that gets triggered by setModel()?
Hi Jay,
Thanks for your kind reply.
`Antoine` is a French Alias Name of me. 😆
No, I don't trigger the reset data binding in any hooks here. I trigger it inside Chrome CONSOLE. Normally this way works on Table, Buttons...
I used the setModel() : sap.ui.getCore().byId("__xmlview0--ChartHolder").setModel(new sap.ui.model.json.JSONModel({}))
Nothing happens.
Yes, we really need to know how SAPUI5 deal with re-rendering mechanism and how data-binding trigger re-rendering.
Best Regards,
Zhunzhun
Aah, I see. 😉
Yeah, a bit better documentation on view and component life cycle and exactly when what events run would be great help. It can be a bit of a puzzle now, with events firing more often than you expect, and sometimes less than you expect (returning to a page that has already been rendered before, for instance).
In order to see these custom controls in action, you may want to check out the demo code here: https://github.com/jay-sdk/SAPUI5-reporting-app
This may specifically help in dealing with issues of updates to the same page/view from a different selection. (specifically the "GrowthRegion" view, where the container is emptied of content before adding the updated SVG in). Maybe not the most elegant approach, but it works.
Hi,
we experiment with your example and have a question. When we instantiate the graph in the onBeforeRendering method, an OData call is triggered to collect the graph data and bind the data to the graph.
In onAfterRendering of Chart.js we don't have the items yet, because the OData request takes a couple ms to be finished. Hence, this.getItems() returns an empty array.
Any idea how to defer the process without using setTimeOut? I see no possibility to use callbacks at the moment, because the code is spread across two files.
Kind regards,
Michael
Hi Michael,
I am not the best person to answer, as I struggle with this myself at times, but will try to steer a colleague your way here on this question.
Hi Jay,
thanks for your great blog, which jumpstarted me on this topic. My questions:
but then oD3 is undefined.
Here's how I did it without VBox/FlexBox:
renderer : function(oRm, oControl) {
var sId = oControl.getId();
oRm.write("<div"); // Control - DIV
oRm.writeControlData(oControl); // writes the Control ID and enables event handling - important!
oRm.writeClasses();
oRm.write('><div id="' + sId + '-svgContainer"></div>');
oRm.write("</div>"); // end Control - DIV
},
onAfterRendering: function(){
var sDomID, oVis;
sDomID = this.getId();
oVis = d3.select("#" + sDomID + "-svgContainer").node();
//Your D3.js code HERE
}
I think creating a custom control just to embed a chart into UI5 application is too much overhead, I've written a blog about another approach, which I think is much easier and superior. If you're interested, check it out.
Hi Jay,
Thanks for the blog. I have followed the blog and created the d3 chart into a custom ui5 control. In the console i am able to see the instance of the custom control but i am not able to see my control.
However, I see the div tags written in my pannel.
Any help will be highly appreciated
Hi,
I could not find the attachement, can some please help me with that?
TIA
Meeti