Technical Articles
Network Graph Visualization in SAP Analytics Cloud Analytic App
In this quick tutorial, we will create an SAC Analytic App custom widget to visualize network graph force layout.
Network Graph
As highlighted in Fiori Design Guidelines, the network graph displays a large amount of data by highlighting the relationships between individual records. Records are displayed as nodes, and connectors (lines) show the relationships between them. Below is the screenshot of the network graph with force layout algorithms.
Prepare the Data
Let’s prepare the data in CSV format. I am getting the data from the SAPUI5 sample Force-Based Layout with Static Nodes. Save it to “data.csv”.
KEY,FROM,TO,TITLE,GROUP,STATUS,ICON,LABEL0,VALUE0,LABEL1,VALUE1
0,0,1,Iron Man,1,Error,sap-icon://key-user-settings,Release date,"May 2, 2008",Director,Jon Favreau
1,1,5,Iron Man 2,1,Error,sap-icon://key-user-settings,Release date,"May 7, 2010",Director,Jon Favreau
2,2,5,The Incredible Hulk,1,Error,sap-icon://theater,Release date,"June 13, 2008",Director,Louis Leterrier
3,3,5,Thor,1,Warning,sap-icon://wrenc,Release date,"May 6, 2011",Director,Kenneth Branagh
4,4,5,Captain America: The First Avenger,1,Success,sap-icon://unfavorite,Release date,"July 22, 2011",Director,Joe Johnston
5,5,6,Marvel's The Avengers,1,Error,sap-icon://text-color,Release date,"May 4, 2012",Director,Joss Whedon
6,5,7,Marvel's The Avengers,1,Error,sap-icon://text-color,Release date,"May 4, 2012",Director,Joss Whedon
7,5,8,Marvel's The Avengers,1,Error,sap-icon://text-color,Release date,"May 4, 2012",Director,Joss Whedon
8,5,19,Marvel's The Avengers,1,Error,sap-icon://text-color,Release date,"May 4, 2012",Director,Joss Whedon
9,6,10,Iron Man 3,2,Error,sap-icon://key-user-settings,Release date,"May 3, 2013",Director,Shane Black
10,7,10,Thor: The Dark World,2,Warning,sap-icon://wrench,Release date,"November 8, 2013",Director,Alan Taylor
11,8,10,Captain America: The Winter Soldier,2,Success,sap-icon://unfavorite,Release date,"April 4, 2014",Director,Anthony & Joe Russo
12,9,12,Doctor Strange,3,Error,sap-icon://activate,Release date,"November 4, 2016",Director,Scott Derrickson
13,10,12,Avengers: Age of Ultron,2,Error,sap-icon://text-color,Release date,"May 1, 2015",Director,Joss Whedon
14,10,13,Avengers: Age of Ultron,2,Error,sap-icon://text-color,Release date,"May 1, 2015",Director,Joss Whedon
15,10,14,Avengers: Age of Ultron,2,Error,sap-icon://text-color,Release date,"May 1, 2015",Director,Joss Whedon
16,10,19,Avengers: Age of Ultron,2,Error,sap-icon://text-color,Release date,"May 1, 2015",Director,Joss Whedon
17,,,Ant-Man and the Wasp,3,Error,sap-icon://chain-link,Release date,"July 6, 2018",Director,Peyton Reed
18,12,20,Thor: Ragnarok,3,Warning,sap-icon://wrench,Release date,"November 3, 2017",Director,Taika Waititi
19,13,11,Ant-Man,2,Error,sap-icon://chain-link,Release date,"July 17, 2015",Director,Peyton Reed
20,13,14,Ant-Man,2,Error,sap-icon://chain-link,Release date,"July 17, 2015",Director,Peyton Reed
21,14,16,Captain America: Civil War,3,Success,sap-icon://unfavorite,Release date,"May 6, 2016",Director,Anthony & Joe Russo
22,14,17,Captain America: Civil War,3,Success,sap-icon://unfavorite,Release date,"May 6, 2016",Director,Anthony & Joe Russo
23,15,18,Guardians of the Galaxy,2,Error,sap-icon://shield,Release date,"August 1, 2014",Director,James Gunn
24,16,20,Spider-Man: Homecoming,3,Error,sap-icon://tree,Release date,"July 7, 2017",Director,Jon Watts
25,17,20,Black Panther,3,Error,sap-icon://circle-task-2,Release date,"February 16, 2018",Director,Ryan Coogler
26,18,20,Guardians of the Galaxy Vol. 2,3,Error,sap-icon://shield,Release date,"May 5, 2017",Director,James Gunn
27,,,Avengers 4,3,Error,sap-icon://text-color,Release date,"May 3, 2019",Director,Anthony & Joe Russo
28,20,19,Avengers: Infinity War,3,Error,sap-icon://text-color,Release date,"April 27, 2018",Director,Anthony & Joe Russo
Expose the Data
Write an R script with Plumber library to expose the data as an REST API, so we can call it from the SAC custom widget app.
suppressMessages(library(plumber))
suppressMessages(library(dplyr))
suppressMessages(library(tidyr))
suppressMessages(library(sqldf))
suppressMessages(library(rlist))
#' @filter cors
cors <- function(req, res) {
res$setHeader("Access-Control-Allow-Origin", "*")
if (req$REQUEST_METHOD == "OPTIONS") {
res$setHeader("Access-Control-Allow-Methods","*")
res$setHeader("Access-Control-Allow-Headers", req$HTTP_ACCESS_CONTROL_REQUEST_HEADERS)
res$status <- 200
return(list())
} else {
plumber::forward()
}
}
#* @param df data frame of variables
#* @get /hierarchy
#* @serializer unboxedJSON
function() {
tryCatch({
data <- read.csv("data.csv")
dataset <- sqldf("SELECT * FROM data")
totalrow = nrow(dataset)
print(totalrow)
node <- list()
line <- list()
group <- list()
count_node <-1
count <- 1
# Getting nodes
for(REC in 1:totalrow) {
KEY <- dataset[REC,"KEY"]
FROM <- dataset[REC,"FROM"]
TO <- dataset[REC,"TO"]
TITLE <- dataset[REC,"TITLE"]
GROUP <- dataset[REC, "GROUP"]
STATUS <- dataset[REC, "STATUS"]
ICON <- dataset[REC, "ICON"]
LABEL0 <- dataset[REC, "LABEL0"]
VALUE0 <- dataset[REC, "VALUE0"]
LABEL1 <- dataset[REC, "LABEL1"]
VALUE1 <- dataset[REC, "VALUE1"]
# Get attributes
attr1 <- list()
attr1 <- c(attr1, label=LABEL0)
attr1 <- c(attr1, value=VALUE0)
attr2 <- list()
attr2 <- c(attr2, label=LABEL1)
attr2 <- c(attr2, value=VALUE1)
attrF <- list()
attrF <- c(attrF, list(attr1))
attrF <- c(attrF, list(attr2))
data <- list()
data <- c(data, key=count_node-1)
data <- c(data, title=TITLE)
data <- c(data, group=GROUP)
data <- c(data, status=STATUS)
data <- c(data, icon=ICON)
data <- c(data, attributes=list(attrF))
if(REC > 1) {
x <- list.find(node, title==TITLE, 1)
if(length(x) == 0 ) {
node[count_node] <- list(data)
count_node <- count_node + 1
}
} else {
node[count_node] <- list(data)
count_node <- count_node + 1
}
# Get lines
if(!is.na(FROM)) {
data_line <- list()
data_line <- c(data_line, from=FROM)
data_line <- c(data_line, to=TO)
line[count] <- list(data_line)
count <- count + 1
}
}
#group
group1 <- list()
group1 <- c(group1, key=1)
group1 <- c(group1, title="Phase One")
group1 <- list(group1)
group2 <- list()
group2 <- c(group2, key=2)
group2 <- c(group2, title="Phase Two")
group2 <- list(group2)
group3 <- list()
group3 <- c(group3, key=3)
group3 <- c(group3, title="Phase Three")
group3 <- list(group3)
group <- list()
group <- c(group, group1)
group <- c(group, group2)
group <- c(group, group3)
# Getting final result
dataF <- list()
dataF <- c(dataF, nodes=list(node))
dataF <- c(dataF, lines=list(line))
dataF <- c(dataF, groups=list(group))
list(dataF)
}, error = function(err){
print(err)
list(err)
})
}
Let’s execute the script to get the result.
SAC Analytic App Custom Widget
Finally, we’ll create a custom widget with SAPUI5 library sap.ui.core.mvc.XMLView and sap.suite.ui.commons.networkgraph.Graph. For more detailed steps how to create, you can refer to the blog in the “Reference” section.
On onInit() function, we execute the Ajax “GET” to request the data from the REST API.
onInit: function() {
var this_ = this;
if (that._firstConnection === 0) {
that._firstConnection = 1;
$.ajax({
url: 'http://127.0.0.1:8856/hierarchy', //R Plumber REST API URL
type: 'GET',
async: true,
timeout: 0,
contentType: 'application/json',
success: function(data) {
var oGraph,
oModel = new JSONModel(data[0]);
oModel.setSizeLimit(Number.MAX_SAFE_INTEGER);
this_.getView().setModel(oModel, that.widgetName);
this_.oModelSettings = new JSONModel({
maxIterations: 200,
maxTime: 500,
initialTemperature: 200,
coolDownStep: 1
});
this_.getView().setModel(this_.oModelSettings, "settings");
this_.oGraph = this_.byId("graph_" + widgetName);
this_.oGraph._fZoomLevel = 0.75;
},
error: function(e) {
console.log("error: " + e);
console.log(e);
}
});
}
}
Create an Analytic app, load the widget and run it. You should see the output as shown below.
References
- Build a Custom Widget in SAP Analytics Cloud, Analytics Application
- github.com/ferrygun/SACAnalyticAppNetworkGraph
Hi,
Very cool! And I like the Marvel Universe dataset 🙂
Possible to share a full screenshot of what the finish product looks like?
Thanks.
Hello Ferry,
Thanks for this great idea and explanation. Unfortunately all seems to work on my side until the part where I add the widget to an App and run it afterwards:
Is there a chance that something has been changed in the meantime?
Thanks for your answer.
Kind regards!