Skip to Content
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%20Page%20example

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: 

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%20Lane%20Properties

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,]);

Step 5: Create a button

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 – Open Story URL API

Help SAP – Using Result Set APIs (with code samples)

SAP Analytic Application Developer Handbook

 

Hope this can enlighten your day!

 

 

 

 

 

 

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

      • Passing filter line values from one Analytic Application to another Analytic Application.
        • Pros: It's quite straightforward to reuse the existing standard content, and it does cater for property values.
        • Cons: Not ideal if you are seeking to minimise customisations and focus on building more stories other than Analytic Applications

      • Add a scripted cascading function in your landing application to derive the property member values.

      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.

      // Get distinct product member of the table widget
      var selections = Table.getDataSource().getDataSelections();
      var memberIds = ArrayUtils.create(Type.string);
      for (var x = 0; x < selections.length; x++) {
          var member = Table.getDataSource().getResultMember("ZP_WBSELEMENT", selections[0]);
      	console.log(["member",member,]);
      	
          if (member && member.id && memberIds.indexOf(member.id) < 0) {	
              memberIds.push(member.id); 
          }
      }

       

       

      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.