In Creating a Tableau Extension / Part One we introduced you to Git, GitHub, Yarn, Visual Studio Code as well as Bootstrap, jQuery and the Tableau Extension API library. In Creating a Tableau Extension / Part Two we learned how to retrieve data from a worksheet and to display this data using a third-party visualisation library, Chart.js, yep, we built an interactive doughnut chart. In Creating a Tableau Extension / Part Three we built a configuration mechanism to allow users to set values to control the extension.

In this final tutorial, we will we work putting into place listeners to enable our Extension to listen to changes, and actions, within our Tableau Dashboard.

Part 4: Listeners

The way that Listeners work is that we can register our Extension to listen to events that will be sent by Tableau. As such, we will need to code in what events we want to listening to, as well as the mechanism to stop listening.

Our starting point for this tutorial will be the output of Creating a Tableau Extension / Part Three. If you have not done so, open up your directory, or if you have skipped the first three parts, use Git to clone the repository to your local machine:

git clone https://github.com/tableaumagic/tableau-extensions-tutorial-part-three

Now, as mentioned in part three, we already added a listener to our extension to ensure that when settings change, the extension will redraw. Let us explore how we achieved this by opening up the application.js and looking at the first several lines of code:

'use strict';

(function () {
   $(document).ready(function () {
      tableau.extensions.initializeAsync({ 'configure':configure }).then(function () {
         drawChartJS();
         unregisterSettingsEventListener = tableau.extensions.settings.addEventListener(tableau.TableauEventType.SettingsChanged, (settingsEvent) => {
               drawChartJS();
            });
         }, function () { console.log('Error while Initializing: ' + 
      err.toString());
   });
});

So to break this down:

  • We must initialize our Tableau Extension by calling the initializeAsync function.
  • Within the initializeAsync, we call tableau.extension.settings.addEventListener function and save a function pointer to unregisterSettingsEventListener.
    • The addEventListener has two parameters which are:
      • tableau.TableauEventType.SettingsChanged which indicates that we want this listener to listen for settings changes.
      • A function that would be executed when an event of SettingsChanged occurs.
      • Calling unregisterSettingsEventListener() would tell the extension to stop listening to SettingsChanged events.

This is the basic mechanics of how we can listen to events within the Tableau Dashboard and update our Extension accordingly. Now let us explore the additional three listeners that are available to us:

  • FilterChanged – listen to events when filters are changed on your workbook. This allows quick filters to control your Extension.
  • MarkSelectionChanged – listen to events when mark selections are changed within your visualisation. This allows your Tableau Extension to respond to mark selections.
  • ParameterChanged – listen to events that are fired when parameters are changed within your dashboard. This allows your Tableau parameters to control your Extension.

We are going to add some more listeners to our Extension.

Note: You will want to save your unregister function pointers in a place where you will be able to call them in the rest of your code.

FilterChanged

Let us change the application.js to be the following:

'use strict';

(function () {
   let unregisterFilterEventListener = null;
   let worksheet = null;
   let worksheetName = null;
   let categoryColumnNumber = null;
   let valueColumnNumber = null;

   $(document).ready(function () {
      tableau.extensions.initializeAsync({ 'configure':configure }).then(function () {
         // Draw the chart when initialising the dashboard.
         getSettings();
         drawChartJS();
         // Set up the Settings Event Listener.
         unregisterSettingsEventListener = tableau.extensions.settings.addEventListener(tableau.TableauEventType.SettingsChanged, (settingsEvent) => {
            // On settings change.
            getSettings();
            drawChartJS();
         });
      }, function () { console.log('Error while Initializing: ' + err.toString()); });
   });

   function getSettings() {
      // Once the settings change populate global variables from the settings.
      worksheetName = tableau.extensions.settings.get("worksheet");
      categoryColumnNumber = tableau.extensions.settings.get("categoryColumnNumber");
      valueColumnNumber = tableau.extensions.settings.get("valueColumnNumber");

      // If settings are changed we will unregister and re register the listener.
      if (unregisterFilterEventListener != null) {
         unregisterFilterEventListener();
      }

      // Get worksheet
      worksheet = tableau.extensions.dashboardContent.dashboard.worksheets.find(function (sheet) {
         return sheet.name===worksheetName;
      });

      // Add listener
      unregisterFilterEventListener = worksheet.addEventListener(tableau.TableauEventType.FilterChanged, (filterEvent) => {
         drawChartJS();
      });
   }

   function drawChartJS() {
      worksheet.getSummaryDataAsync().then(function (sumdata) {
         var labels = [];
         var data = [];
         var worksheetData = sumdata.data;
         for (var i = 0; i<worksheetData.length; i++) {
            labels.push(worksheetData[i][categoryColumnNumber-1].formattedValue);
            data.push(worksheetData[i][valueColumnNumber-1].value);
         }

      var ctx = $("#myChart");
      myChart = new Chart(ctx, {
         type: 'doughnut',
         data: {
            labels: labels,
            datasets: [{
backgroundColor: ["#3e95cd", "#8e5ea2", "#3cba9f", "#e8c3b9", "#c45850"],
            data: data
            }]
         }
      });
     });
   }

   function configure() {
      const popupUrl=`${window.location.origin}/dialog.html`;
      let defaultPayload="";
      tableau.extensions.ui.displayDialogAsync(popupUrl, defaultPayload, { height:300, width:500 }).then((closePayload) => {
         drawChartJS();
      }).catch((error) => {
         switch (error.errorCode) {
            case tableau.ErrorCodes.DialogClosedByUser:
            console.log("Dialog was closed by user");
            break;
         default:
            console.error(error.message);
         }
      });
   }
})();

Check the comments in the code for the update, it is quite a big one.

Let us test this change by:

  • Opening up our previously developed Tableau Workbook.
  • Add a quick filter to our source worksheets.
  • Add this new Extension and point to the worksheet. 
  • Change the quick filter and see how the Extension will change.

Note: We are rebuilding the Chart.js visualisation each time we change the quick filter. With a bit more time and logic we would be able to animate the data change, but that will be for another day.

With our Extension now listening to Filter Changes, I would like to add a listener for mark changes.

MarkSelectionChanged

Let us change the application.js to be the following:

'use strict';
 
(function () {
 
   let unregisterFilterEventListener = null;
   let unregisterMarkSelectionEventListener = null;
   let worksheet = null;
   let worksheetName = null;
   let categoryColumnNumber = null;
   let valueColumnNumber = null;
 
   $(document).ready(function () {
      tableau.extensions.initializeAsync({ 'configure':configure }).then(function () {
         // Draw the chart when initialising the dashboard.
         getSettings();
         drawChartJS();
         // Set up the Settings Event Listener.
         unregisterSettingsEventListener = tableau.extensions.settings.addEventListener(tableau.TableauEventType.SettingsChanged, (settingsEvent) => {
            // On settings change.
            getSettings();
            drawChartJS();
         });
      }, function () { console.log('Error while Initializing: ' +err.toString()); });
   });
 
   function getSettings() {
      // Once the settings change populate global variables from the settings.
      worksheetName = tableau.extensions.settings.get("worksheet");
      categoryColumnNumber = tableau.extensions.settings.get("categoryColumnNumber");
      valueColumnNumber = tableau.extensions.settings.get("valueColumnNumber");
 
      // If settings are changed we will unregister and re register the listener.
      if (unregisterFilterEventListener != null) {
         unregisterFilterEventListener();
      }

      // If settings are changed we will unregister and re register the listener.
      if (unregisterMarkSelectionEventListener != null) {
         unregisterMarkSelectionEventListener();
      }
 
      // Get worksheet
      worksheet = tableau.extensions.dashboardContent.dashboard.worksheets.find(function (sheet) {
         return sheet.name===worksheetName;
      });
 
      // Add listener
      unregisterFilterEventListener = worksheet.addEventListener(tableau.TableauEventType.FilterChanged, (filterEvent) => {
         drawChartJS();
      });

      unregisterMarkSelectionEventListener = worksheet.addEventListener(tableau.TableauEventType.MarkSelectionChanged, (filterEvent) => {
         drawChartJS();
      });
   }
 
   function drawChartJS() {
      worksheet.getSummaryDataAsync().then(function (sumdata) {
         var labels = [];
         var data = [];
         var worksheetData = sumdata.data;
   
         for (var i=0; i<worksheetData.length; i++) {
            labels.push(worksheetData[i][categoryColumnNumber-1].formattedValue);
            data.push(worksheetData[i][valueColumnNumber-1].value);
         }

         var ctx = $("#myChart");
         myChart = new Chart(ctx, { 
            type: 'doughnut', 
            data: { 
               labels: labels, 
               datasets: [{ 
                  backgroundColor: ["#3e95cd", "#8e5ea2", "#3cba9f", "#e8c3b9", "#c45850"],
                  data: data
               }]
            }
         });
      });
   }

   function configure() {
      const popupUrl=`${window.location.origin}/dialog.html`;
      let defaultPayload="";
    
      tableau.extensions.ui.displayDialogAsync(popupUrl, defaultPayload, { height:300, width:500 }).then((closePayload) => {
         drawChartJS();
      }).catch((error) => {
         switch (error.errorCode) {
            case tableau.ErrorCodes.DialogClosedByUser:
               console.log("Dialog was closed by user");
               break;
            default:
               console.error(error.message);
         }
      });
   }
})();

As you can see we have not made too many changes to the application.js, but what we did do is the following:

  • Added a variable called unregisterMarkSelectionEventListener that will allow us to unregister our listener.
  • Updated the getSettings function to listen to the MarkSelectionChanged event type.

As with above, now test your Extension on the Tableau Workbook. Your Extension should now update based on a change in Quick Filters and also when you select data marks on the visualisation.

Note: FilterChanged and MarkSelectionChanged event listeners are applied to a worksheet. SettingsChanged is applied to an Extension. 

There is only one more task to look at, which is ParameterChanged and how to set up the listener for that. However, this is something I would like you to look into and discover for yourself, after all, it is the best way of learning and you can find the required information here: https://github.com/tableau/extensions-api/tree/master/Samples/Parameters, and also via the official API Reference here: https://tableau.github.io/extensions-api/docs/index.html

You can find my repository for this project here: https://github.com/tableaumagic/tableau-extensions-tutorial-part-four

Summary

In this tutorial, we looked at modifying our Extension to listen to events fired by Tableau, captured these events, and then updated our Extension. The four main event types are:

  • SettingsChanged
  • FilterChanged
  • MarkSelectionChanged
  • ParameterChanged

All I can say now is go forth and build your extensions and do not forget to send it to me, I would love to know what you all build. and with that said, it is a wrap for this lengthy series on how to create a Tableau Extension. We also looked at:

4 COMMENTS

  1. thank you, thank you! Your efforts in putting this altogher are a great help as it’s cohesive and dissected into logical chunks. I had already partially got there by reading through TREX doc which is about as pleasant as meeting up with a real TREX, and looking at sample code supplied with the API. You’re right that it can be a game changer, hopefully it will be. Do you have any seminars / webinars to cover this topic?

  2. Hi Toan,
    As some one who is getting started with Tableau Extensions, your articles have been very helpful.
    Are there any technologies you suggest to build an app that can write back to a database, eg. SQL Server or Vertica?

    Thanks

    • Keep in mind that an Extension is a web application. Therefore, there are two ways of doing this. 1) Building a web application which can talk to a database, and then adding the Tableau Extension API for interaction, or 2) Building a web service onto of your database, and then allowing your extension to post to it. Either way, it would require solid web development.

LEAVE A REPLY

Please enter your comment!
Please enter your name here

This site uses Akismet to reduce spam. Learn how your comment data is processed.