Using Tableau JavaScript API to Fill Parameter from a Selection

Editor’s Note: Parameter Actions solves this natively in Tableau without any additional effort! Ignore everything below and just upgrade!

Tableau’s actions are very powerful for creating interactive visualizations, but there are a couple of common use cases that are not possible out of the box in Tableau Desktop.

  1. An Action cannot cause a Parameter to be filled with the results of that selection
  2. An Action from a Primary Data Source cannot be a Filter on a Secondary Data Source.

Using the JavaScript API, you can easily solve the first issue, and from there solve issue #2.

Potential use cases include geospatial calculations where a selected point can become the new reference longitude and latitude for the calculation, or when using data blended onto Custom Polygons.

Initial Setup of Embedded Viz using JS API

A few prerequisites before we get started: To use the JS API, you must be embedding the viz into an HTML page. Everything shown here relies on the existence of a “wrapper” page. Luckily, it doesn’t take much to have a basic environment that allows embedding.

Your basic JS API embed framework starts like this

<html>
<head>
    <title>Using Selection to Power Parameter</title>
    
    http://localhost/javascripts/api/tableau-2.0.0.min.js
    
    
        var viz_url = "http://{tableau server}/views/..."
        var param_name = '';
        var selection_field_name = '';  
        var worksheets_to_listen_on = {'Sheet 1' : true, 'Sheet 2' : true};
        console.log(worksheets_to_listen_on);
        var viz;
        var book;
        var activeSheet;
        
        function initViz(){
            var placeholderDiv = document.getElementById("tableauViz");
            var options = { 
                hideTabs: false,
                hideToolbar: true,
                // onFirstInteractive is a callback that happens once the Viz is loaded
                onFirstInteractive: function () {
                  book = viz.getWorkbook();
                  activeSheet = book.getActiveSheet();
                }
            };
            
            // Creates the actual viz
            viz = new tableau.Viz(placeholderDiv,viz_url,options);
}   
</head>
<body onload="initViz();">

</body> </html>

All this does it get your viz in the page, but using the JS API so that other actions are possible.

Setting a Parameter’s value

Setting a parameter is simple enough. You refer to it by the


var param_name = 'Parameter Name Exactly As In Tableau Desktop';

var value = 'Some Value';

viz.getWorkbook().changeParameterValueAsync(param_name, value).then(
function (){ console.log('Parameter set');}
);

You’ll notice the .then() method with a callback function. The Tableau JS API is based on the Promises framework for asynchronous calls, which you can read more about in the documentation. The quickest way to think of this is that for any method with ‘Async’ in the name, you can put a callback function in the .then() method to happen AFTER the first action has finished happening. You can chain these together as well. All I’m doing is logging to the console so I kn0w the action actually completed.

Getting the Values from Selected Marks

Our goal is to detect the value of a selection, and then put that value into the parameter. Tableau Parameters can only take a single value, while selections can grab any number of items, so we also want to only change the parameter value when a single value is selected. The last important action to consider is the “deselection” when nothing is selected.

The JavaScript API allows you to create an Event Listener on the viz object, listening for actions. In this case, we want the ‘marksselection’ action.


viz.addEventListener("marksselection",getMarks);

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();

// Here you can route for specific worksheets based on the object defined at the beginning
if ( worksheets_to_listen_on[ws_name]) {
console.log('Marks selection being routed from ' + ws_name);
e.getMarksAsync().then( handleMarksSelection );
}
}

This lets you choose which Sheets you are listening to and then route all of those selected marks to the handleMarksSelection function. Here I am only creating one route (and also only from one worksheet), but you could easily create multiple routes this way.

Now we’ve got a MarksSelection object ‘m’ passing to our handleMarksSelection(m) function. The MarksSelection object is a collection of Marks objects with a numeric index. Each Marks object has a collection of Pairs. The Pair object has three properties: fieldName, formattedValue and value. You access them directly without any setter / getter type methods.

Put more simply, you work through from the MarksSelection -> Marks ->Pairs -> Pair properties. I tend to add methods to the MarksSelection object to work with the data in a more ‘relational’ way. For example,


// Easiest way to get the Values for a given field is to add the following method to the MarksSelection object
// Pass the text value of the Field Name as fName and will return array of all of those values from the selected mark
m.getValuesForGivenField = function(fName){
var valuesArray = new Array();

// Run through each Mark in the Marks Collection
for(i=0;i&amp;amp;lt;this.length;i++){
pairs = this[i].getPairs();
for(j=0;j&amp;amp;lt;pairs.length;j++){
if( pairs[j].fieldName == fName) {
valuesArray.push( pairs[j].formattedValue );
// Alternatively you could get the value not formattedValue
// valuesArray.push( pairs[j].value );
}
}
}
return valuesArray;
}

values = m.getValuesForGivenField(selection_field_name); // Replace with the name of the field you want
console.log(values);

This is a clean way to get the ordered list of the values that were selected for any given field name.

So the whole handleMarksSelection function I build to take a single marks selection and put that value into a parameter is


function handleMarksSelection(m){

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

// Cleared selection detection
if(m.length == 0){
//$("#running_action_history").fadeOut();
// Reset to 'All' if no selection

viz.getWorkbook().changeParameterValueAsync(param_name,'All').then(
function (){ console.log('Parameter set back to All');}
);
return;
}

// MarksSelection object is a collection of Marks class with numeric index
// Marks contain Pairs, accessed by getPairs(), which is also collection
// Easiest way to get the Values for a given field is to add the following method to the MarksSelection object
// Pass the text value of the Field Name as fName and will return array of all of those values from the selected mark
m.getValuesForGivenField = function(fName){
var valuesArray = new Array();

// Run through each Mark in the Marks Collection
for(i=0;i&amp;amp;lt;this.length;i++){
pairs = this[i].getPairs();
for(j=0;j&amp;amp;lt;pairs.length;j++){
if( pairs[j].fieldName == fName) {
valuesArray.push( pairs[j].formattedValue );
// Alternatively you could get the value not formattedValue
// valuesArray.push( pairs[j].value );
}
}
}
return valuesArray;
}

values = m.getValuesForGivenField(selection_field_name); // Replace with the name of the field you want
console.log(values);

// Then do your thing with that array of Values
if (values.length === 1){

viz.getWorkbook().changeParameterValueAsync(param_name,values[0]).then(
function (){ console.log('Parameter set');}
);
}

Building the Parameter and Creating the Filter

To make this work, you need to actually have a Parameter created in the Tableau workbook. For Dimensional filters, String is usually the correct type. Set the default value to ‘All’

Now you’ll need a calculation to tie the Parameter into the Filter. This is not very different from the standard instructions in the Tableau Knowledge Base but the calculation will be slightly more complex to allow for the ‘All’ option. If using this to work around Secondary Data Sources when Blending, make sure to make this calculation on the Secondary Data Source.

IF [Param] = ‘All’ THEN 1
ELSEIF [Field Name]=[Param] THEN 1
ELSE 0
END

Make this calculation a Dimension, then place it on the Filters shelf and set the value to 1.

Publish to Tableau Server. Now go into your HTML page and update the framework with the correct view location, the Parameter Name, and the worksheet names you want to listen to. When you make a single selection on the viz, it should change the Parameter value to match, which will work almost like an action filter.

I’ve put the full code of this framework up on GitHub here

 

Leave a comment