Technical Articles
How to drive your SAC stories based on a landing Analytic Application filter line
This blog post will demonstrate how you could create a state-of-the-art SAC landing Analytic Application, which will enable users to drive their planning processes consistently based on a filter line.
What’s a landing page?
Landing Page example
I’m a firm believer in landing pages for Planning processes. It’s a great way to create a seamless experience for planners.
A landing page is a standalone story or Analytic application in SAC context, designed with a single focus or goal. In a planning context, a goal could be to perform the budgeting processes for your projects or plan your workforce costs for the FY22.
SAC standard content library includes some great content with this concept in mind. Workforce Planning is one of them. I would highly recommend activating this content and having a look.
To take this approach to the next level, I will demonstrate how to create a SAC Analytic Application that includes a filter line and pass these filter values to the SAC stories.
For context, I would suggest looking into the references below:
- Blog post – Hyperlinking SAC Analytic Application to Story by dynamically passing selected dimension value as a filter
- Help SAP – SAC Filter Parameters (Model_Id, Story_Id, Page_Id, etc)
The blog post above covers the concept of passing selected dimension values from a widget to a Story as a filter. In my case, I’m taking this example and adopting a filter line, which needs further scripting as end-users can dynamically change it based on the underline model.
To start, what I would recommend:
- Obviously, some basic Javascript knowledge. If I was able to put it off, you should be just fine
- Activate the Analytic Application “Learn how to set variables and/or filters and open the receiver application” (Under Analytics Designer Features). I used the coding from this application as a baseline for this development.
Step 1: Add a chart or table to act as a data source
As the filter line widget requires a source Widget, you have to add a chart or a table in the Analytic Application. Use the chart or table, like I did, to your benefit by providing a high-level overview of your planning KPI’s.
In my example, I named my table as “Table”.
Step 2: Add the filter line
Filter Line Properties
Step 3: Create Script variables
These script variables act as Global variables, thus used across Script objects:
Script Variables | Type | Array |
Dim_array | String | X |
Hier_array | String | X |
ModelID | String | |
Opt_array | String | X |
OverallCount | Integer | |
PageID | String | |
StoryID | String | |
Unbooked_array | String | X |
URLParam_filters | String | |
URLParam_Variables | String | |
Val_array | String | X |
Step 4: Create object scripts
Create the following object scripts.
Code samples:
To identify the Story ID, Model ID and Page ID, I’d suggest looking into the help page below:
function ReportIDMapping(ButtonCall: String): void
//function maps the story, model and page ID based on the button name
switch(ButtonCall)
{
case "Resource Planning":
StoryID = <StoryID>;
ModelID = <ModelID>;
PageID = <PageID>;
break;
case "Simulate Depreciation":
<<add your parameters here>>
}
Utils.openReceiverApp(StoryID, ModelID, PageID);
function openReceiverApp(StoryID: String, ModelID: string, PageID: string): void
//function executes the URL
var ds = Table.getDataSource();
var separator = "|";
Dim_array = [""];
Hier_array = [""];
Opt_array = [""];
Val_array = [""];
OverallCount = 0;
Unbooked_array = [""];
UrlParam_Filters ="";
UrlParam_Variables = "";
Utils.getUrlParam_Filters(ds);
//if any selection is performed
if (OverallCount > 0)
{
var urlparam = UrlParameter.create("f01Model",ModelID);
var url_array = [urlparam];
//dimension array
for (var i = 0; i < Dim_array.length; i++) {
var val_values = Dim_array[i].split(separator);
//console.log(["urlarray1",val_values,]);
urlparam = UrlParameter.create(val_values[0],val_values[1]);
url_array.push(urlparam);
}
//opt array
for (i = 0; i < Opt_array.length; i++) {
val_values = Opt_array[i].split(separator);
urlparam = UrlParameter.create(val_values[0],val_values[1]);
url_array.push(urlparam);
}
//values array
for (i = 0; i < Val_array.length; i++) {
val_values = Val_array[i].split(separator);
//console.log(["urlarray3",val_values,]);
urlparam = UrlParameter.create(val_values[0],"["+val_values[1]+"]");
url_array.push(urlparam);
}
//hierarchy array
for (i = 0; i < Hier_array.length; i++) {
val_values = Hier_array[i].split(separator);
//console.log(["urlarray3",val_values,]);
urlparam = UrlParameter.create(val_values[0],val_values[1]);
url_array.push(urlparam);
}
//unbooked array
for (i = 0; i < Unbooked_array.length; i++) {
val_values = Unbooked_array[i].split(separator);
//console.log(["urlarray3",val_values,]);
urlparam = UrlParameter.create(val_values[0],val_values[1]);
url_array.push(urlparam);
}
}
NavigationUtils.openStory(StoryId,PageID,url_array, true);
function getURLParam_Filters(ds: datasource): void
// function loops through dimension filters and execute
var dimensions = ds.getDimensions();
for (var i=0;i<dimensions.length; i++){
//get dimension
var dimension = dimensions[i];
//get dimensionfilters
var filters = ds.getDimensionFilters(dimension);
// check filter values and if Measure is filtered
if (filters.length > 0 && dimension.id !== "@MeasureDimension") {
//console.log(["Filters", filters,]);
var hierarchy = ds.getHierarchy(dimension).id;
//get filter function
Utils.getFilter(dimension.id,filters,hierarchy,i);
}
}
function getFilter(dimension: string, filters: FilterValue[], hierarchy: string, dimindex: integer): void
//Function populates Dim, Hier, Opt and Val arrays
var s = "";
var v_separator = "|";
var v_dim = "";
var v_opt = "";
var v_hier = "";
var v_unbooked = "";
var v_optvalue = "in";
// count required for parameters
if ( filters.length > 0 )
{
OverallCount = OverallCount + 1;
}
for (var i = 0; i < filters.length; i++) {
var filter = filters[i];
// create variable to store dimension parameters for URL
var v_str = ConvertUtils.numberToString(OverallCount);
//dimension parameters
v_dim = "f0" + v_str + "Dim";
Dim_array.push(v_dim + v_separator + dimension);
//hierarchy parameters
v_hier = "f0" + v_str + "Hierarchy";
Hier_array.push(v_hier + v_separator + hierarchy);
//unbooked parameters
v_unbooked = "f0" + v_str + "Unbooked";
Unbooked_array.push(v_unbooked + v_separator + "true");
if (filter.type === FilterValueType.Single) {
// handle single filter value
var singleFilter = cast(Type.SingleFilterValue, filter);
//opt parameters
if (singleFilter.exclude === true)
{
v_optvalue = "notIn";
} else
{
v_optvalue = "in";
}
v_opt = "f0" + v_str + "Op";
Opt_array.push(v_opt + v_separator + v_optvalue);
if (hierarchy !== "@FlatHierarchy")
{
//split values
var v_trim = singleFilter.value.split("[").join("");
v_trim = v_trim.split("]").join("");
v_trim = v_trim.split("&").join("");
if (dimension === "Date") //Date comes in a different structure
{
var array = v_trim.split(".");
s = s + '"' + array[4] + '"';
}else
{
array = v_trim.split(".");
s = s + '"' + array[2] + '"';
}
} else
{
s = s + '"' + singleFilter.value + '"';
}
} else if (filter.type === FilterValueType.Multiple) {
// handle multiple filter value
var multipleFilter = cast(Type.MultipleFilterValue, filter);
var values = multipleFilter.values;
//opt parameters
if (multipleFilter.exclude === true)
{
v_optvalue = "notIn";
} else
{
v_optvalue = "in";
}
v_opt = "f0" + v_str + "Op";
Opt_array.push(v_opt + v_separator + v_optvalue);
for (var j = 0; j < values.length; j++) {
if (j > 0) {
s = s + ",";
}
// check hierarchy selection
if (hierarchy !== "@FlatHierarchy")
{
//split values
v_trim = values[j].split("[").join("");
v_trim = v_trim.split("]").join("");
v_trim = v_trim.split("&").join("");
if (dimension === "Date") //Date comes in a different structure
{
array = v_trim.split(".");
s = s + '"' + array[4] + '"';
}else
{
array = v_trim.split(".");
s = s + '"' + array[2] + '"';
}
} else //flat presentation
{
s = s + '"' + values[j] + '"';
}
}
}
Val_array.push("f0" + v_str + "Val" + v_separator + s);
}
//if you want to print the arrays
//console.log(["val_array",Val_array,]);
//console.log(["Dim_array",Dim_array,]);
//console.log(["opt_array",Opt_array,]);
//console.log(["Hier_array",Hier_array,]);
If you notice in the function ReportIDMapping, the case statement is based on the text of the button. In the example below, the button description “Resource Planning” is sent as a parameter to the “ReportIDMapping function.
Step 6: Add “OnClick” script to the button
function OnClick() void
Utils.ReportIDMapping(ResourcePlanning.getText());
Voila! Your Analytic Application is now sending the filter parameters dynamically to your story.
Summary:
From a technical perspective, you have learned how to create a simple Analytic Application that includes: a filter lane, table, and a button that executes a set of Javascript functions calling a destination story.
Moreover, you learned how landing pages could support your customers in taking full advantage of SAC, by enabling a seamless and simplified experience.
I hope you can leverage this example to many other use cases, exploring the many advantages of SAC Analytic Application API’s.
Lastly, I would love to hear your comments and feedback below. For any further technical questions, I would recommend having a look or post questions on the SAC community questions page.
Key references:
Help SAP – Filters Parameters guide
Help SAP – Using Result Set APIs (with code samples)
SAP Analytic Application Developer Handbook
Hope this can enlighten your day!
Hi Reinaldo, great post!
I've tried something similar but ran into 2 limitations: filling story filter of a level-based hierarchy and also passing a property to story filter (e.g. filter costcenters based on a company code property) didn't work... (not supported by story filters).
Thanks Jef. Good callout.
I couldn't crack passing attribute values to a Story filter. Alternatively, I see a couple of options:
This would depend on your requirement. For instance, in your case, if you seeking to filter the cost centres based on a company code property, then you could create a filter lane with company code filter, then try reading the properties of the cost centre based on the filter lane and push the cost centre values to the URL. I haven't developed the full code, but the function below could be helpful:
Please note the selections[0] returns the property value.
I will keep you posted and, keep me posted if you have any thoughts or ideas.
Thanks for the reply!
Indeed. For now this forces us to use analytic applications instead of stories.
When passing all the (cost center) members, not sure what the URL & story filter limits are when it needs to pass hundreds of members, but that doesn't feel right. 🙂
Yes agreed, passing hundreds of members doesn't feel right and it might not be the right use case.
I had this concept in mind for high-level filters, such as company code, version and time. For more detailed dimensionality, such as WBS or Customer, these would need to rely heavily on the hierarchy nodes to minimise the number of members.