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:

  • Renaming the TutorialTwo.trex to TutorialThree.trex
  • Update some details in the Tableau Extension manifest file.
    • Description
    • Author name
    • Author email
    • Author organisation
  • We will now add an icon file to the manifest.
    • Get a PNG image that is a 64×64 pixel wide (it must be exact and Base64 encoded)
    • Go to the following website, upload the image and copy the Base64 text that represents your image.
    • Paste this code inside the icon tag.
    • The Tableau Magic icon is iVBORw0KGgoAAAANSUhEUgAAAEAAAABAAQMAAACQp+OdAAAABlBMVEUAAAAAAAClZ7nPAAAAAXRSTlMAQObYZgAAACdJREFUKJFjYKAS+EA04z8hxn9qMfg/UIEBJAcVg4hw/g8F+BhUAgCAtrS1v85ACwAAAABJRU5ErkJggg==
  • Save this file and then open your extension within Tableau Desktop.

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 modified the Tableau Extension manifest file to include more details about us as developers, as we get closer to the end of this tutorial, we should be thinking about how we will release our work.
  • Added an icon to our extension for that little extra professional touch; branding counts more than you would believe. #TableauMagic
  • We added an additional tag to the Tableau Extension manifest file to let Tableau know we want a configuration window.
  • We added a call to a function in our initializeAsync to configure(); the configure function will:
    • Specify the location of the configuration window HTML; we use a relative location here as we don’t know where this code will be deployed.
    • Call the displayDialogAsync function while passing in an optional payload.
  • We added the dialog.html which:
    • Prints out a message.
    • Imported jQuery and Bootstrap.
    • Linked to an external css file called dialog.css.

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:

  • Pick the Worksheet that will be used as a data source.
  • Pick the column in the data source that will be used as the category.
  • Pick the column in the data source that will be used as the value.
  • We would like to be able to save this information so that when we open up the Workbook, the data is automatically reloaded and doughnut charts render what we expect.
  • When we close and reopen our Configuration window, we want our chosen values to appear there.

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:

  • Build the Configuration Dialogue Window.
  • Save (and return) the values to settings.
  • Update the Visualisation based on data.

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:

  • Create a New Tableau Workbook.
  • Connect to Sample Superstore.
  • Create a new worksheet called Sales by Region.
    • Drag Region onto Columns.
    • Drag Sales onto Rows.
  • Create a new worksheet called Sales by Category.
    • Drag Category onto Columns.
    • Drag Sales onto Rows.
  • Drag your newly created extension into your dashboard, configure this and now try to run.

And that is it, we are done with part three. You can find the full source code here: https://github.com/tableaumagic/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

  • Change the Doughnut Type in the application.js to bar and see the results. Yes, it is that easy, how about changing it to pie, or let us be even more daring. In your own time:
    • Add an extra option to the configuration dialogue which allows you to choose between doughnut, pie and bar chart.
    • Persist this additional information into the Extension Settings.
    • Use the chart type to render different charts based on user settings. 

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

5 COMMENTS

  1. Hi,

    After placing two sheets on dashboard and extension, after selecting one sheet for a extension and then switching to the other one I get weird bug when hovering over chart, chart switches from one sheet data to the second one.

    Any help on this one? I even tried only GitHub version and it’s the same.

    Thanks!

      • 2019.2 and 2019.1 Desktop Version.
        Well I tried DataTables extension and no, it’s not happening there but the format of that outcome is not really a chart as we have here.

        Here’s a GIF what’s happening when hovering.
        https://gifyu.com/image/EEmF

        I’ve used your github version of extension, on more than one PC 🙁

        • Maybe it could be up to that “we” are never actually deleting old chart but I am not sure where should we destroy the old one.

  2. Hi ,

    Thank you !
    Can you please share final view after adding extension ? i would like to see how much viz enhanced and visualization difference after adding extensions in stories/dashboards.

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.