Holy Sheets: Understanding the Tableau Server JavaScript API

I love the JavaScript API tutorial on Tableau’s site, but even though this is my day job, the JavaScript API Reference still takes me in circles sometimes before I’ve figured out exactly how to solve a specific problem. I’m going to share what I’ve learned over the years, both explanations and some code for solving common problems.

How did I find these things out? There is the API Reference, as well as the Concepts page, which both provide different views and examples. I’ve paired that with a lot of testing using Firefox with Firebug and Chrome’s developer tools. Often the best way to understand what is happening is to do console.log(object); on a particular variable and compare against the API Reference to understand exactly what class you are dealing with at that moment.

Detecting if a Sheet is a Dashboard, Worksheet, or Story

Most Tableau Server documentation refers to whatever is published and visible to an end user as a View. The equivalent of a View in the JavaScript API is a Sheet. A Sheet can in fact be a Dashboard, Worksheet or a Story.

If you do Workbook.getActiveSheet(), you’ll receive back a Sheet object. To know how to handle this properly, you should then use the getSheetType() method and compare as such:

sheet = viz.getWorkbook().getActiveSheet();
switch (sheet.getSheetType()) {
case 'worksheet':
break;
case 'dashboard':
worksheets = sheet.getWorksheets();
for(i=0;i<worksheets.length;i++){
worksheets[i]. // Do anything you'd like with a given worksheet
}
break;
case 'story':
break;
}

 Worksheets in a Dashboard

This is not at all obvious without looking at the resulting object in some sort of browser developer tools, but when you get a Worksheet object from the getWorksheets() method, it actually has all of the methods of a Sheet class and a Worksheet class. Further, the actual methods on a Worksheet class are divided into three sections in the documentation: the Worksheet class reference, the Marks Selection section, and the Worksheet Class(Filtering) section. Depending on what you’d like to do, make sure you go to the correct section to understand the methods available.

For example, you can use getName() to get the name of the sheet in the dashboard; this method comes from the Sheet class. You can also use getSelectedMarksAsync() , which is a method of the Worksheet Class, but covered in the documentation on Mark Selection.

If you want to work with a specific sheet within a dashboard (and you will need to do certain actions, like exporting a CSV crosstab), you need to iterate through all of the sheets to find what you are looking for (expanding on our earlier example):

sheet = viz.getWorkbook().getActiveSheet();
switch (sheet.getSheetType()) {
case 'worksheet':
break;
case 'dashboard':
worksheets = sheet.getWorksheets();
for(i=0;i<worksheets.length;i++){
 switch(worksheets[i].getName()){
case 'SheetIWantToUse':
worksheets[i].getFiltersAsync().then (
function (f){ // Use the filter objects to do things });
break;
}
}
break;
case 'story':
break;
}

Different Classes Handle Different Functions (Parameters are at Workbook level, etc.)

Parameters in Tableau have values that apply across an entire workbook; therefore they are also manipulated via the Workbook class. Custom Views are also managed via the Workbook class. Most of the other actions that are available via the toolbar are managed through the Viz class. Filters?

One thing to consider is that calls to change things such as Parameters and Filters are Async() and must run in a one-at-a-time fashion. This can lead to a considerable amount of time spent where the Viz is inactive and reloading. If you have a whole set of them you would like to configure at the beginning, you can pass the values for filters and parameters at load time through the options object. You can also programmatically toggle the Automatic Updates configuration before and after sending your calls using the Viz.toggleAutomaticUpdatesAsync() method.

Asynchronous Calls

The first hurdle, which is very well documented here is the asynchronous nature of many of the calls. If you are new to JavaScript and web development, this will probably be your biggest hurdle. Luckily, every asynchronous method in the Tableau JS API has the word Async in the name. What is an Asynchronous call? Put succinctly, because some actions take time for the Tableau Server to process, the calls are sent in a way that you don’t sit around waiting with the screen all locked up while the Tableau Server does its work (you can look up the concept of AJAX to understand this more generally). Instead, you specify a callback function which will only do its thing once the Tableau Server is finished and serves back a response.

What do you need to know about callback functions? Let’s compare to a few regular, non-async methods:

var active_sheet = Workbook.getActiveSheet();
// At this point immediately after the getActiveSheet(),  active_sheet now is the Sheet object
sheet_name = active_sheet.getName();
// At this point, sheet_name is a string with the sheet name
console.log(sheet_name);

If you’ve done any programming, this will feel familiar. You trigger off an action through a function/method, and store the result in a variable. It’s called synchronous because if it takes any time for the result to come back, nothing else will happen in the browser (sometimes this behavior is referred to as “blocking”; modern browsers pop up warnings when things block for too long).

An Async method in the Tableau JS API does return a value, but it is just a Promise object, and it won’t contain anything useful to you at the moment. Instead, you need to specify a callback function that will accept the results, whenever they do come back, and do things from there. For example, let’s try and get the parameters that are set in the workbook using getParametersAsync():

parameters_array = Workbook.getParametersAsync(); // This is wrong
console.log(parameters_array); // Will give you a Promise object, not an array of the parmaters
// Correct way with callback
workbook.getParametersAsync(
   // this is an anonymous function, but you could also have a named function
   // p represents whatever is returned from the Tableau Server. Check the reference
   function(p){
       console.log(p); // I do this just to confirm what comes back.
       // In this case, p is an array of Parameter objects
       for(i=0;i<p.length;i++){
          // You can find the methods for the Parameter object in the Reference Guide
          p_name = p[i].getName();
          p_value = p[i].getCurrentValue(); // This is DataValue object
          p_actual_value = p_value.value; // DataValue has value and formattedValue fields (not methods)
          p_formatted_value = p_value.formattedValue;
          console.log('Parameter ' + p_name + ' has the value ' + p_formatted_value);
       }
   });

Event Listeners for Deep Integration

Using Event Listeners, actions in a Tableau viz can be “captured” and then information used to drive additional actions, whether in the Tableau viz (setting a Parameter value from the value of a selection)  or in the existing web application (a dialog for editing the selected records that writes back to the database).

The docs are pretty clear on how these are used, but here’s my own run-through. Here is the correct code to enable each of the event listeners:


viz.addEventListener(tableau.TableauEventName.MARKS_SELECTION, getMarks);
viz.addEventListener(tableau.TableauEventName.FILTER_CHANGE, getFilter );
viz.addEventListener(tableau.TableauEventName.TAB_SWITCH, getTab );
viz.addEventListener(tableau.TableauEventName.PARAMETER_VALUE_CHANGE, getParam );
viz.addEventListener(tableau.TableauEventName.STORY_POINT_SWITCH, getStoryPoint );
viz.addEventListener(tableau.TableauEventName.CUSTOM_VIEW_REMOVE, getRemovedCustomView );
viz.addEventListener(tableau.TableauEventName.CUSTOM_VIEW_SAVE, getSavedCustomView );
viz.addEventListener(tableau.TableauEventName.CUSTOMER_VIEW_SET_DEFAULT, getSetDefaultCustomView );

 

You’ll notice that each of these points has a particular event name (a “fully qualified enum” as the Tableau Docs call it) and a callback function. The callback is where you define what you want to happen. There will always be a single object returned, so your callback functions should be defined with one argument like:


function getMarks(e){

    worksheet = e.getWorksheet(); // e in this case is a MarksEvent Class

   e.getMarksAsync().then( function(m) { } ); // Marks are retrieved through Async() method


 

Marks Selections

If you are working on integrating with an existing application, you can go down several paths for how to deal with selections:

  1. Listen on the selection events, take action on anything that is selected. You can determine the number of marks that are selected; which allows you to only take action on single-selections if that makes more sense
  2. Listen on the selection events, but simply store the information about the selections in a “queue”. Then have a button that takes action on those items when in the queue
  3. Have something in your web application UI (a link or button) which triggers the Worksheet.getSelectedMarksAsync() method when clicked, then does the action on those marks at that time. The same array of Marks object types is returned by the event handler or the getSelectedMarksAsync() method.

Each Mark object has a method getPairs() which returns an array of Pair objects. Each pair represents one of each of the fields that is available in the overall Level of Detail of the Tableau viz. So if you have SUM([Sales]) on Rows, Year([Order Date]) on Columns, AVG([Profit]) on Color, with [Product Category] and [Product Sub-Category] on Tooltip, there will be a Pair object for each of those.

Once you get working with this paradigm, you’ll realize very quickly that the JS API is not rows and columns oriented liked Tableau itself, but actually object-oriented. If you want to do things over the whole of a column, you’ll need to transform the data a bit. Personally, I use the following code to add some methods to the MarksSelection object early on, which are built to parse through all of the Mark and Pairs and give useful JavaScript arrays as outputs.


// Generic Wrapper to Handle Selected Marks
// Two Cases -- Clear Selection (0 Marks) and Selection
function getMarks(e){
e.getMarksAsync().then(
function(m){
console.log("[Event] Marks selection, " + m.length + " marks");
console.log(m);
/*
// Cleared selection detection
if(m.length == 0){
//$("#running_action_history").fadeOut();
handleClearedSelection(m);
return;
}
*/
addMarksFunctions(m);

handleMarksSelection(m);

}
);
}

function addMarksFunctions (m){
// MarksSelection object is a collection of Marks class with numeric index
// Marks contain Pairs, accessed by getPairs(), which is also collection
// Get all Field Names from first member of collection
// Adds getFieldNames method to the object
m.getFieldNames = function () {
var field_names = new Array();
pairs = this[0].getPairs();
for(j=0;j<pairs.length;j++){
field_names.push(pairs[j].fieldName);
}
this.field_names = field_names;
return field_names;
}

// Returns array of all actual values for a given field
m.getValues = function(field_name) {
return m.getFromField(field_name, 'value');
}

// Returns array of all formatted values for a given field
m.getFormattedValues = function(field_name){
return m.getFromField(field_name, 'formatted_value');
}

// Generic function to return array of all values in order for a given field
m.getFromField = function (field_name,value_type){
var value_array = new Array();
console.log(this);
for(i=0;i<this.length;i++){
pairs = this[i].getPairs();
console.log(this[i].getPairs());
console.log("Selected Mark " + i + " , " + pairs.length + " pairs of data");
for(j=0;j<pairs.length;j++){
// Pair has three properties: fieldName, formattedValue, value, accessed DIRECTLY, without setter / getter method
/* Enable for debugging
console.log(
"Pair " + j + " -- " +
"Field Name: " + pairs[j].fieldName + " , " +
"Value: " + pairs[j].value + " , " +
"Formatted Value: " + pairs[j].formattedValue
);*/
if( pairs[j].fieldName === field_name ) {
if(value_type == 'value'){
value_array.push( pairs[j].value );
}
if(value_type == 'formatted value') {
value.array.push( pairs[j].formattedValue ) ;
}
}
}
}
return value_array;
}

m.getArrayOfPairObjects = function (){
var obj_array = new Array();
for(i=0;i<this.length;i++){
pairs = this[i].getPairs();
obj_array.push(pairs);
}
return obj_array;
}
}

You’ll notice near the top two functions that are called, handleClearedSelection(m) and handleMarksSelection(m). Those would be the two functions you would put your code in to do whatever you want with the MarksSelection object. You can think of this a bit of indirection — rather than having the initial callback from the event listener do what I want, I instead use it to “dress-up” the MarksSelection object with some new methods that will help me when I finally do the actions I want in the handleMarksSelection(m) method.

I may expand out these helper functions as Tableau 10 comes out, because there will be additional getData() methods that also return using the Pairs concept.

MarksSelection with Dashboards – Determining where the original click was made

If you have Dashboard actions on a Dashboard, you will notice that when you select marks that trigger an action, there will actually be a MarksSelection event for each of the affected worksheets in the dashboard, because they do all update based on the action. However, you can tell which sheet the marks were actually selected on by using the getMarksAsync() method of the MarksSelection object. If there were zero marks, then that is not the selected worksheet.


function getMarks(e){
console.log('Result of getMarks:');
console.log(e);
var ws = e.getWorksheet();
console.log('Worksheet obj:');
console.log(ws);
var ws_name = ws.getName();
console.log('Worksheet is named : ' + ws_name);

e.getMarksAsync().then( handleMarksSelection );

}
function handleMarksSelection(m){

console.log("[Event] Marks selection, " + m.length + " marks");
console.log(m);

// Cleared selection detection
if(m.length > 0){
// This is your selected worksheet
}

 

Leave a comment