Monday, August 5, 2013

Tag Cloud SAS Server Page Components - Part 2 (of 2)

I recently posted about an AJAX and JQuery based Tag Cloud demo. I next described the re-usable AJAX template that is the foundation for the demo. In my last post I described two of the four component SAS Server Pages. This post completes the series with a description of:
  • tagCloud.html: the SAS Server Page that generates and displays the tag cloud (seen in left hand side of figure 1).
  • tagCloudJSON.sas: the SAS Macro the creates the JSON (JavaScript Object Notation) used to populate the tag cloud.
  • cloudDrill.sas: the SAS Macro that displays the drill-down results (seen in the right hand side of figure 1) when the user clicks on a word in the tag cloud.
Figure 1. Tag Cloud, including drill-down results.

The tagCloud SAS Server Page

The JQCloud JQuery widget is used to generate the tag cloud so we first have to define the style sheet and the JavaScript code to the page. Note the PROC STREAM readfile directive is used to do that. It simply includes the contents of the two files as is. Note that we could have also downloaded and put these files in our web server directory tree.

<html>
 <head>
  <style>&streamDelim readfile srvrpgs(jqcloud.css);</style>
  <script>&streamDelim readfile srvrpgs(jqcloud-1.0.3.js);</script>
 </head>


Next, we make sure that the parameters that define the tag cloud exist as macro variables and have default values.

 <body>
  %global data statistic word weight;
  %let data = %sysfunc(coalescec(&data,sashelp.shoes));
  %let statistic = %sysfunc(coalescec(&statistic,sum));
  %let word = %sysfunc(coalescec(&word,Subsidiary));
  %let weight = %sysfunc(coalescec(&weight,Sales));


In order to make the page completely stand-alone, the code to summarize the data and calculate the weight to apply to each word is executed using the dosubl function, invoked via the %sysfunc macro.

  %let rc = %sysfunc(dosubl(
               proc summary data = &data nway;
                class &word;
                var &weight;
                output out=cloud &statistic=;
               run;
            ));

Next a div tag is used to define the display area for the the tag cloud. And id value of cloudCanvas is used so the tagCloud plug-in can reference this area of the display. Note that the width, height, and border parameters could be parameterized using macro variables.

The newline directive is used (here and in the JavaScript below) to ensure that PROC STREAM does not insert a line break that could potentially cause problems. The use of the newline directive in this way should be considered a required Best Practice.

  &streamDelim newline;<div id="cloudCanvas" style="float:left;
                            width: 600px; height: 400px;
                            border: 1px solid #ccc; margin-bottom:5px">
                       </div>

The data for the tag cloud is specified in a JavaScript variable (called tagCloud) whose value is a JSON string (the macro that generates the JSON string is discussed below).

  &streamDelim newline;<script>
  &streamDelim newline; var tagCloud =

                             %tagCloudJSON
                                 (data=cloud
                                 ,word=&word
                                 ,weight=&weight
                                 )

Next is the JQuery syntax that says the the jQCloud JavaScript code should use the tagCloud variable as the source of the JSON used generate the display for the cloudCanvas div.

  &streamDelim newline; $(function() {
  &streamDelim newline;   $("#cloudCanvas").jQCloud(tagCloud);
  &streamDelim newline; });


Next is the JQuery syntax that detects when a word has been clicked on. Here, we call the cloudDrill macro and display the results using AJAX in the drillResults region of the screen.

  &streamDelim newline; $("#cloudCanvas").click(function(e) {
  &streamDelim newline;   var selected = $(e.target).text();
  &streamDelim newline;   $("#drillResults").load("&_url?_debug=&_debug%nrstr(&)_program=&_metafolder.runMacro%nrstr(&)macroToRun=cloudDrill%nrstr(&)data=&data%nrstr(&)class=&word%nrstr(&)word="+encodeURIComponent(selected));
  &streamDelim newline; });
  &streamDelim newline;</script>


And finally, is the definition of the area, with some default initial text, where the resulting drill down table is displayed.

  <div style="float:left;  margin-left:10px;" id="drillResults">
   <br>Click on a word/phase in the tag cloud to see the detail.
   The drill-down report will be displayed to the right of the

   Tag Cloud if there is enough horizontal space. Otherwise it
   will be displayed below the Tag Cloud.
  </div>
 </body>
</html>


That's all there is to it. While the JQuery syntax takes some getting used to; it really is fairly straightforward.

The tagCloudJSON Macro

A macro is used to read the data set and generate the needed JSON.

%macro tagCloudJSON
      (data = _last_
      ,where = 1
      ,word = word
      ,weight = weight
      );


The SCL data access functions are used via the %sysfunc macro function.

 %local dsid wordNum weightNum weightFmt comma;
 %let dsid = %sysfunc(open(&data(where = (&where))));
 %let wordNum = %sysfunc(varnum(&dsid,&word));
 %let weightNum = %sysfunc(varnum(&dsid,&weight));
 %let weightFmt = %sysfunc(varfmt(&dsid,&weightnum));


We loop through the data set reading each observation generating the needed JSON.
  • the text attribute is the value of the word (the class variable in SAS terminology).
  • the weight attribute is the unformatted value of the analysis variable.
  • And the html attribute is a nested JSON string the specifies that the hover text should be the formatted value of the analysis variable. And since the word is clickable, we make sure that the pointer attribute is used when the user hovers over a word.
The comma macro variable is set to blank initially and then reset so that each observations JSON is comma separated.

 %let comma=;
 &streamDelim newline;[

 %do %while(%sysfunc(fetch(&dsid))=0);
    &comma &streamDelim newline;
    {
      text:   "%qtrim(%qsysfunc(getvarc(&dsid,&wordnum)))"
    , weight: %qsysfunc(getvarn(&dsid,&weightNum))
    , html:   { title: "%qsysfunc(getvarn(&dsid,&weightNum),&weightFmt)"

              , style: "cursor:pointer"
              }
    }
    %let comma = ,;
 %end;


And finally, we close the data set and close the JSON string.

 %let dsid = %sysfunc(close(&dsid));
 &streamDelim newline; ];

%mend tagCloudJSON;

The cloudDrill Macro 

This macro is executed by the runMacro stored process whenever the user clicks on a word. Three parameters are passed to it:
  • The name of the data set (referenced here as &data) that provided the source data for the tag cloud.
  • The name of the class variable that is the value for the words (referenced here as &class).
  • The actual value the user clicked on (referenced here as &word).
Note also that the class variable is displayed in the PROC REPORT spanned header and so it is defined using the noprint define statement attribute so screen real estate is not used/wasted to display it on each row.

 %macro cloudDrill;
 options nocenter;
 proc report data = &data nowd split='_' missing;
  columns ("&Word" _all_);
  where &class = "&word";
  define &class / noprint;
  rbreak after / summarize;
 run;

%mend cloudDrill;

Summary

I hope that this all makes sense. If not, feel free to post a question.

I plan to use the AJAX template discussed here and in my last three posts in future blog posts that illustrate additional examples of what can be done. So please make sure to check them out.