Toan Hoang

Creating a Tableau Extension / Part Three

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 this tutorial, we will we work on building a configuration dialogue window so that we can customise our data extension, choose which worksheet your data will come from, and more importantly, save your configurations.

Part 3: Configuration

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

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

Now that you have done that, I want us to explore the Tableau Extension Manifest file and we will start by:

Note: all of the above information will be visible when someone is using your extension, so do give some information about the source of your extension.

Now that we are warmed up, we will inform Tableau that our Extension will have a configuration dialogue window; we do this by adding the following information to the .trex file after the permissions closing tag (</permissions>):

<context-menu>
   <configure-context-menu-item/>
</context-menu>

We will now need to modify our application.js file to include a Configuration dialogue as well as the HTML code for the dialogue window.

application.js

'use strict';
 
(function () {
   $(document).ready(function () {
      // Added new code here to point to the configure function.
      tableau.extensions.initializeAsync({ 'configure':configure }).then(function () {
         drawChartJS();
      }, function () { console.log('Error while Initializing: ' + err.toString()); });
   });
 
   function drawChartJS() {
      const worksheets=tableau.extensions.dashboardContent.dashboard.worksheets;
      var worksheet=worksheets.find(function (sheet) {
         return sheet.name==="worksheetData";
      });
      
      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][0].formattedValue);
            data.push(worksheetData[i][1].value);
         }

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

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

Finally, we will create a Cascading Style Sheet (css) file in a new css folder, css/dialog.css:

body {
   padding: 20px;
}

Finally, remove and re-add your Extension to your dashboard, click on the down arrow and you should now see a Configure… button; click on Configure… to see your dialog.html window appear with your hello message. 

15 Second Recap

We can let our imagination go wild with this newly created configuration dialogue window, but how do we get information back to our Tableau Extension code? and how do we save this so that it is there when we save and reopen our dashboard? This is where the Settings comes in.

Settings

The Settings namespace allows you to get and set values that can be used to configure your extension; a setting is a key/value pair (setting name and setting value), and are persisted in your workbook. Let us extend our Workbook example so that we will display two Chart.js doughnut charts side by side but using a different (configured) worksheet as a data source. This means that in our Configuration Dialogue window, we will need to be able to:

There is going to be a bit of coding ahead and we are going to lean heavily on javascript and jQuery, but be not afraid. We will not do the following:

Let us update the dialog.html to look like the following:

<html>
<head>
    <link rel="stylesheet" type="text/css" href="libs/bootstrap/dist/css/bootstrap.css">
    <link rel="stylesheet" type="text/css" href="css/dialog.css">
</head>
<body>
    <div class="container">
        <div class="page-header">
            <h1>Tableau Extension / <small> Settings</small></h1>
        </div>
        <div class="row">
            <div class="col-md-6 col-xs-6">
                <span class="dropdown-label">Worksheet Data Source</span>
                <select id="selectWorksheet" class="select">
                    <option disabled selected="selected">-- None Selected --</option>
                </select>
            </div>
        </div>
        <div class="row">
            <div class="col-md-6 col-xs-6">
                <span class="dropdown-label">Select Category</span>
                <select id="selectCategory" class="select">
                    <option disabled selected="selected">-- None Selected --</option>
                </select>
            </div>
        </div>
        <div class="row">
            <div class="col-md-6 col-xs-6">
                <span class="dropdown-label">Select Value</span>
                <select id="selectValue" class="select">
                    <option disabled selected="selected">-- None Selected --</option>
                </select>
            </div>
        </div>
        <div class="row dialog-buttons">
            <span id="error"></span>
            <button id="save" class="btn btn-success">Save</button>
            <button id="cancel" class="btn btn-danger">Cancel</button>
        </div>
    </div>
</body>
<script type="text/javascript" src="libs/jquery/dist/jquery.min.js"></script>
<script type="text/javascript" src="libs/bootstrap/dist/js/bootstrap.min.js"></script>
<script type="text/javascript" src="js/tableau-extensions-1.0.0.min.js"></script>
<script type="text/javascript" src="js/dialog.js"></script>
</html>

This is standard bootstrap code, which we will add some additional style by updating the dialog.css to:

.container {
    padding: 0px;
}

.row {
    margin: 10px;
}

.page-header {
    background-color: #DDD;
    padding: 10px;
}

.btn {
    margin: 0 10 0 0;
}

.dialog-buttons {
    margin-top: 100px;
}

Now for the functionality, we will also add a dialog.js file:

'use strict';

(function () {

    $(document).ready(function () {
        tableau.extensions.initializeDialogAsync().then(function (openPayload) {
            buildDialog();
        });
    });

    function buildDialog() {
        let dashboard = tableau.extensions.dashboardContent.dashboard;
        dashboard.worksheets.forEach(function (worksheet) {
            $("#selectWorksheet").append("<option value='" + worksheet.name + "'>" + worksheet.name + "</option>");
        });
        var worksheetName = tableau.extensions.settings.get("worksheet");
        if (worksheetName != undefined) {
            $("#selectWorksheet").val(worksheetName);
            columnsUpdate();
        }

        $('#selectWorksheet').on('change', '', function (e) {
            columnsUpdate();
        });
        $('#cancel').click(closeDialog);
        $('#save').click(saveButton);
        $('.select').select2();
    }

    function columnsUpdate() {

        var worksheets = tableau.extensions.dashboardContent.dashboard.worksheets;
        var worksheetName = $("#selectWorksheet").val();

        var worksheet = worksheets.find(function (sheet) {
            return sheet.name === worksheetName;
        });      

        worksheet.getSummaryDataAsync({ maxRows: 1 }).then(function (sumdata) {
            var worksheetColumns = sumdata.columns;
            $("#selectCategory").text("");
            $("#selectValue").text("");
            var counter = 1;
            worksheetColumns.forEach(function (current_value) {
                $("#selectCategory").append("<option value='" + counter + "'>"+current_value.fieldName+"</option>");
                $("#selectValue").append("<option value='" + counter + "'>"+current_value.fieldName+"</option>");
                counter++;
            });
            $("#selectCategory").val(tableau.extensions.settings.get("categoryColumnNumber"));
            $("#selectValue").val(tableau.extensions.settings.get("valueColumnNumber"));
        });
    }

    function reloadSettings() {
        
    }

    function closeDialog() {
        tableau.extensions.ui.closeDialog("10");
    }

    function saveButton() {

        tableau.extensions.settings.set("worksheet", $("#selectWorksheet").val());
        tableau.extensions.settings.set("categoryColumnNumber", $("#selectCategory").val());
        tableau.extensions.settings.set("valueColumnNumber", $("#selectValue").val());

        tableau.extensions.settings.saveAsync().then((currentSettings) => {
            tableau.extensions.ui.closeDialog("10");
        });
    }
})();

ok, so that was intense, however, all we are logically doing is using Tableau to populate the three drop down boxes, and then adding some functionality to save the values to settings.

Now that we have a mechanism for saving and retrieving settings, we now need to modify our application.js to read and act on these values:

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

  function drawChartJS() {

    var worksheetName = tableau.extensions.settings.get("worksheet");
    var categoryColumnNumber = tableau.extensions.settings.get("categoryColumnNumber");
    var valueColumnNumber = tableau.extensions.settings.get("valueColumnNumber");

    const worksheets=tableau.extensions.dashboardContent.dashboard.worksheets;
    var worksheet=worksheets.find(function (sheet) {
      return sheet.name===worksheetName;
    });
    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");
      var 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);
      }
    });
  }
})();

and that about does it… we now have an Extension that draws a Chart.js doughnut chart based on worksheets on your dashboard. Now test it out and see how it goes for you.

Testing our Extension

Let us test this by:

And that is it, we are done with part three. You can find the full source code here: https://github.com/simplifyinsights/tableau-extensions-tutorial-part-three

Summary

This has been a longer than expected tutorial to write, but I hope you enjoy reading this as much as I enjoyed writing it. We have now gone through setting up our development environment, using a third party library to render a chart based on data within a Tableau Dashboard, and finally ensuring that our extension is configurable. I said this in previous articles, but the Tableau Extension API is a real game changer.

Now I am looking forward to closing off this series in part four which will introduce listeners and also the ability to filter Tableau worksheets from your customer visualisation.

Extra Credit

I hope you enjoyed this Tutorial and look forward to the other parts of this series where we will go through the following:

If you like our work, do consider supporting us on Patreon, and for supporting us, we will give you early access to tutorials, exclusive videos, as well as access to current and future courses on Udemy:

Also, do be sure to check out our various courses:

Exit mobile version