Different Drillthroughs for Graph Elements

I recently received an interesting problem. A multi bar graph needed to have drillthroughs pointing to separate reports. The requirement is to have it seamless for the end-user, no transitional screens. If the user clicks on the revenue bar, it needs to go to the revenue report. If they click on the planned revenue bar, it goes to the planned revenue report.

As the product is currently built, drillthroughs are defined on the graph level, not a measure level. Let’s take a look at the actual HTML being generated:

separateDrillsDefaultChart

<area 
  dttargets="<drillTarget drillIdx=\"2\" label=\"Planned revenue\"/><drillTarget drillIdx=\"3\" label=\"Revenue\"/>" 
  display="914,352,803.72" 
  tabindex="-1" 
  ctx="27::22::18" 
  title="Year = 2010 Revenue = 914,352,803.72" 
  coords="157, 309, 157, 174, 118, 174, 118, 310, 157, 310" 
  shape="POLY" 
  type="chartElement" 
  class="dl chart_area" 
  ddc="1" 
  href="#"
>

In this example, I have a chart with two bars. In the area of each bar, the dttargets is defined with both drills. The drills themselves I’ve named the same as the data item of the measure. We can then use JavaScript to extract the dttargets string, match the label of the drill to the data item name, and place the correct one in there.

/*
 * This will loop through every chart and replace multiple drill definitions with one. 
 */
paulScripts.fixChartDrills = function(chartName){
  var oCV = window['oCV'+'_THIS_']
  , areas = paulScripts.getElement(chartName).parentNode.previousSibling.getElementsByClassName('chart_area')
  , areasLen = areas.length
  , areaDataItemName
  , drills=[]
  , dtargets=[]
;

for (var i=0;i<areasLen;++i){
  if(!areas[i].getAttribute('dttargets')) continue;
  areaDataItemName=oCV.getDataItemName(areas[i].getAttribute('ctx'));

  drills = areas[i].getAttribute('dttargets');
  dtargets =drills.split('>');

  for (var j=0;j<dtargets.length;++j){
    var regexp = /label...(.+?)."/g;
    var match = regexp.exec(dtargets[j]);

    if(match&&match[1] == areaDataItemName) areas[i].setAttribute('dttargets',dtargets[j]+'>');

  }

First we’re finding the chart that we want to do this on, and finding the area map. We loop through the areas, skipping the ones that don’t have any dttargets (like the legend or labels).
For each area with a dttarget, we get the source data item name. Fortunately for us, there’s a useful Cognos JavaScript function to do it! Then a little hackey JS magic to get the label for each individual dttarget we can finally match and replace the dttargets attribute.

Let’s see it in action!
separateDrills

Now it’s very important that the drillthroughs have exactly the same names as the data items. If they don’t do that, this script won’t work – but of course that wouldn’t stop you from using different logic. I built this in Cognos 10.2.2, but I have no reason to think it’s not backwards compatible. The full JavaScript, including the paulScripts.getElement can be found in the report XML below.

separateDrills.txt (152 downloads)

Guest Post: A New Take on Date Range Prompts

Since version 10.2 of IBM Cognos BI Suite, IBM included an API to access and manipulate prompt objects. Since prompt objects are the main instrument we use to allow users to communicate with a report (Interactivity or user selection), being able to manipulate them however we see fit can change user experience dramatically for the better. There are countless examples of how the prompt API can be used to achieve this. For example, dynamic defaults: Suppose you have two prompts, for region and for products. You want the default product selected to be the best selling product in the region selected. With prompt API, this can be achieved easily.

In this post I’d like to showcase one of the first solutions I ever wrote using Prompt API, because it was one of the things I wanted to solve for a long time.

Every so often we add “from date” and “to date” prompts to a report, to use for filtering the report output to show only data from the date range selected. The problem is, most users and most use cases don’t require the sort of flexibility a date range offers: most users will not run their sales-per-branch report between April 23rd and May 2nd, for instance, because it’s an arbitrary chunk of dates. Instead, users are likely to filter dates for last month, this MTD, QTD, YTD, last week and so on. So, basically, set, standard, comparable time frames. And sometimes the date range prompt can be replaced with a drop down list of such pre-set ranges, but other times, users ask to still have the flexibility of choosing to and from date, but nonetheless, still mostly use the set, comparable ranges.

Now, in order to select last month’s dates with two date prompts, your average user will need 6 clicks: One to open from date calendar, one to page back to last month, one to click on “1”, and the same process with the “To date” prompt. For YTD, they might need more. That’s a lot of clicks. Also, developers often have to write scripts to get the default value right, and because these date prompts are never done in a centralised, reusable manner, they end up writing a script for each report. I have long fought the war on developers wasting time by doing things more than once, and this case is no different. Even if reports require different default times, the solution can still be generalised, and therefore made reusable.

My solution uses JavaScript and Prompt API to add to the date prompt functionality. Here is how it works:

Date Solution

I’m using two date prompts, and adding 7 pre-defined links, which, when clicked, fill in their respective dates. So, for example, clicking on MTD will set the from date prompt to the 1st of the current month, and the to date prompt to today’s date. There’s also a verification mechanism in place to ensure that from date is always earlier than to date, or equal to it.

But how do I make this solution generalised? Let’s take a look at the report studio report:

RS Look

The bit in blue is the error message the user will get if they choose an end date that’s prior to the start date. The bit in blue is a text that should be replaced with another text – containing just one number between 1 and 7, corresponding with a dynamic ate range.  “1” is YTD, 4 is WTD and so on.

Now, if you drag in a layout reference object to this interface, here’s what you’ll get:

override

You can override&replace the warning message and the default text. So, if the default for a certain report is “last month”, you’ll override “Defaults”

replace

Drag in a text item and insert “5”

default set

When you run the report, the default would be last month:

final result

This way you can set a different default value for each report in a reusable manner.

I’m attaching the XML, of course, but pay attention to these caveats:

1. The script has seven preconfigured date ranges. You can change them or add to them as you require, and use the general example in the code, but it requires some knowledge of scripting. Unfortunately, I will not be able to provide support for such customisations.

2. If you’re relying on my script to manipulate weeks, pay attention that my script assumes Monday is the first day of the week. Israelis especially, this means you’ll have to change this logic (Weeks in Israel begin on Sundays).

3.This is 10.2.2 – You can downgrade it to 10.2.x by changing the version number at the top.

 

daterange.txt (973 downloads)

 

Nimrod (Rod) Avissar is a Cognos expert, with a penchant for specialized UX solutions. Feel free to drop me a line! (LinkedIn).

 

 

Passing parameters without refreshing the page

Recently a reader had an interesting problem. Her client needed a prompt page from which they could open different reports. As it stands now, the client needed to select their prompts, hit the finish button, and then click on the links. She wanted a way to skip the second step – select the prompts, and have those values pass in the drill through.

With the prompt API, and a bit of JS, this is incredibly easy.

The first step is to identify the target report. You can get the report search path through the properties. Click on the “View the search path, ID and URL Location” link, and a window will pop up with the path and other details.
1. Get search path

Notice the double quotes? That can potentially cause trouble with the JavaScript, so I recommend using a URL Encoder: /content/folder[@name=’JavaScript Examples’]/folder[@name=’Drill Target’]/report[@name=’Target’] becomes %2Fcontent%2Ffolder%5B%40name%3D%27JavaScript%20Examples%27%5D%2Ffolder%5B%40name%3D%27Drill%20Target%27%5D%2Freport%5B%40name%3D%27Target%27%5D

The example I’m posting will be using the URL Encoded version.

The next step will be the JavaScript:

<script>

  var paulScripts = {}
    , oCR = cognos.Report.getReport("_THIS_")
    , gateway = window['oCV_THIS_'].getRV().getCV().sGateway
    , report1 = decodeURIComponent("%2Fcontent%2Ffolder%5B%40name%3D%27JavaScript%20Examples%27%5D%2Ffolder%5B%40name%3D%27Drill%20Target%27%5D%2Freport%5B%40name%3D%27Target%27%5D%0A%0A");
 

  /* function paulScripts.URLEncode - Paul Mendelson = 2013-10-21
   * Based on the JSONEncode function, this will return the use/display values of
   * a prompt in a format that can be based via POST or GET.
   * usage: 'p_Parameter1='+paulScripts.URLEncode(paulScripts.getControl('MyPrompt'))
   */
  paulScripts.URLEncode = function(promptControl){
    var urlData = '<selectChoices>'
      , aPromptValues = promptControl.getValues()
      , nonRange = []
      , range =[];

    if (aPromptValues.length == 0) {return false}
   
    for (var j=0; j< aPromptValues.length; j++) {
      var promptValue =  aPromptValues[j];
     
      if (promptValue.use) {// Non Range value
        nonRange.push("<selectOption useValue='"+promptValue.use + "' displayValue='"+ promptValue.display + "'/>");
        }
      else { // Range value   
        var rangeStart = promptValue.start
          , rangeEnd = promptValue.end
          , start, end, startDisp, endDisp;
        if (rangeStart && rangeEnd) { //has both Start and End (bounded)
          rangeStart.display?startDisp = " displayValue='"+ rangeStart.display+"'":"";
          rangeEnd.display?endDisp = " displayValue='"+ rangeEnd.display+"'":"";
          range.push("<selectBoundRange><start useValue='"+rangeStart.use +"'" + startDisp +"/><end useValue='"+rangeEnd.use +"'" + endDisp +"/></selectBoundRange>")
        }
        else if (rangeStart && !rangeEnd) {//unboundedEndRange
          rangeStart.display?startDisp = " displayValue='"+ rangeStart.display+"'":"";
          range.push("<selectUnboundedEndRange><start useValue='"+rangeStart.use +"'" + startDisp +"/></selectUnboundedEndRange>");
        }
        else if (!rangeStart && rangeEnd) {
          rangeEnd.display?endDisp = " displayValue='"+ rangeEnd.display+"'":"";
          range.push("<selectUnboundedStartRange><end useValue='"+rangeEnd.use +"'" + endDisp +"/></selectUnboundedStartRange>");
        }
        else {
          alert ("Range not set.");
        } // end if
      } // end if
    } // end for
    if(nonRange.length>0) urlData += nonRange.join();
    if(range.length>0) urlData += range.join();
    urlData+='</selectChoices>';
    return urlData.replace(/&/g,'&amp;');
  }

  /* function paulScripts.getPrompts
   * Paul Mendelson - 2013-10-21
   * Retrieves all the prompts on the page and returns the parameter values
   */
  paulScripts.getPrompts = function(){
  //find all of the controls and stick them in aPromptControls.
    var aPromptControls = oCR.prompt.getControls( ),
    prompts = [];

  //loop through the controls - finding the name and values and sticking them into the names array
    for(i=0;i<aPromptControls.length;i++)
    {
      if(aPromptControls[i].getValues().length == 0) continue;
      prompts.push(aPromptControls[i].getParameterName());
      prompts.push(paulScripts.URLEncode (aPromptControls[i]));
    }
   
  return  prompts;
  }

paulScripts.runReport = function(reportName,reportPath,gateway, prompts){
  var basicInfo = [
          reportName, "toolbar=no,height=200,width=400"
        , 'ui.gateway' , gateway
        , 'ui.tool' , 'CognosViewer'
        , 'ui.action' , 'run'
        , 'ui.object' , reportPath
        , "run.prompt" ,"false"
        , "cv.toolbar" ,"false"
        , "cv.header" ,"false"
        , "run.prompt", "false"
    ];

    basicInfo=basicInfo.concat(prompts);
  return cognosLaunchInWindow.apply(this,basicInfo);
  }



</script>
 

Let’s go through the functions.

var paulScripts = {}
, oCR = cognos.Report.getReport(“_THIS_”)
, gateway = window[‘oCV_THIS_’].getRV().getCV().sGateway
, report1 = decodeURIComponent(“%2Fcontent%2Ffolder%5B%40name%3D%27JavaScript%20Examples%27%5D%2Ffolder%5B%40name%3D%27Drill%20Target%27%5D%2Freport%5B%40name%3D%27Target%27%5D%0A%0A”);

We’re defining the variables on the page. Notice that I’m hardcoding the report search path into a variable here. There’s actually no reason to do that, we could put it directly into the function, or put it into another prompt, or even insert it into a table and build a list with each row calling another report.

The gateway variable is pulling the gateway from the report you’re running. Another gateway could be entered here, if needed.

The paulScripts.URLEncode will take a prompt control, process the selected values, and spit out a string that Cognos can use to populate parameters.

paulScripts.getPrompts will look for every prompt on the page, and create an array of the parameter names and the selected strings. This makes updating the report simple – you won’t need to modify and JavaScript when adding or removing prompts.

paulScripts.runReport is the last function. It uses the cognosLaunchInWindow function to run a report in the specified window, using the specified search path, on the specified gateway, with the results of the paulScripts.getPrompts function.

To use it, all you need to do is creating an onclick event like this:

<input type="button" onclick="paulScripts.runReport('_blank',report1,gateway, paulScripts.getPrompts())" value="Run Report 1"/>

And finally, we can test it:
passingPrompts

This report was built on 10.2.1, but should work on all versions 10.2 and up.
Passing Parameters Without Refresh (653 downloads)