When I was applying for my first job at Tableau as a Product Consultant, part of the interview process included building a dashboard to show off my skills. I had built an awesome dashboard but I wanted to add something that would really stick out to the interviewers so I decided to make a polar radar chart. I spent hoooouuuurs re-learning the math to create a dynamic polar axis and made a whole bunch of calculations. I got it done and got the job, but if I was to redo that interview again today, I'd probably just build an extension! It's super easy to build and what's even better is that you can use it for any dashboard you want! In this tutorial, I'll be showing you how to use Chart.js to build your own custom charts. Of course, this tutorial can be applied to your favorite visualization library like D3.js, Highcharts, Recharts, and more. The principles will be the same whichever you use so stick with your favorite! Once you've learned how to set up the extension and integrate your favorite visualization library you'll be able to churn out your own custom charts in Tableau!
Want to see what we'll build? Check out the demo workbook and trex file!
To run an extension it needs to be hosted somewhere. If you want to get things up and running quickly get started by remixing my starter template on Glitch. Glitch is a free tool for developing and hosting apps collaboratively. It's also where you can find the finished code for this and many of my tutorials and resources. I recommend creating an account with Glitch so you can save your project but it is not necessary in order to follow along with this tutorial. Once you've remixed the starter template visit https://<your-project-name-here>.glitch.me
and if you see "Hello World!" everything is good to go.
If you'd like to follow along locally instead you can set up a simple http-server to serve your extension:
npm install --global http-server
.http-server -p 8888
.http://localhost:8888
in your browser and you should see "Hello World!" if everything is set up correctly!You will need either Tableau Desktop or web authoring in Tableau Server/Online to test and use your extension. If you want a free developer site to develop your extension with you can join the Tableau Developer Program!
To tie into my interview throwback we'll be using the old Coffee Chain data set Tableau used to ship with back in the day. However, any data source will do.
Tableau Desktop has a built-in debugging tool that can help us troubleshoot any problems we come across while developing our extension. First, close any instances of Tableau Desktop then follow the instructions here to start Tableau Desktop with the debugger running. Note: If you are using 2021.1 or higher you no longer need to use an old version of Chrome. Once you've started Tableau Desktop with the debugger go to http://localhost:8696 (or whichever port you chose) and you should see a list of "Inspectable Pages" and the "Discover Pane". Keep this open so you can debug as we go along. Once you bring in the extension you will see it show up in this list (make sure to refresh!). If you're using Tableau Server/Online you can simply look in the Console tab of your browser's developer tools and refresh the page for updates.
In order to use an extension, you need to have a .trex (pronounced Tee-Rex🦖) manifest file that tells Tableau information about your extension, such as where it is hosted. To quickly create a .trex file you can use my TREX Generator. Make sure you check the "Include configure context menu" option! Once you have the trex file you can drag it into your dashboard and if you are using one of the templates above you should see "Hello World!". (You might see an error about not having a configuration, this will get fixed as we build the extension.)
The first thing we're going to build is the configuration page. Each time you use this extension you'll want it to be based on different data found on different worksheets so we don't want to hardcode any values. Instead, we'll let the dashboard author pick which worksheets and fields should be used in a configuration pop-up window. First, we'll need to add new files for config.html and config.js. You can begin by copying the index.html and script.js files and renaming them appropriately. Next, make sure to link to the config.js file in config.html instead of script.js and bring in the Tableau Extensions API and the Vue.js library we'll be using for the configuration. Vue is a great JavaScript framework and makes it really easy to manage updates without having to write a ton of code. You can learn more about it from this two-minute video. The main thing to know for this tutorial is that we can easily pull in javascript variables into our inputs and they'll update automatically as the data updates. The <head>
tag of your config.html file should include the following:
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script src="https://extensions.tableauusercontent.com/resources/tableau.extensions.1.latest.min.js"></script>
<script src="/config.js" defer></script>
Now we can get started building out the inputs we'll need. Note, I'll be using the Bulma CSS framework for styling so this tutorial can focus on the good bits. You can bring that into your stylesheet by adding the following to the top of the style.css file:
@import 'https://cdn.jsdelivr.net/npm/[email protected]/css/bulma.min.css';
Whether you use either of these frameworks or not we'll need to build four inputs and a button. We need to collect: the source worksheet to build the chart from, the dimension and measure fields to use on that worksheet, and the worksheet we want to filter when you click on the chart. Here's an example of how to write one of these inputs with Vue:
<select v-model="sourceWorksheet">
<option v-for="ws in worksheets" v-bind:value="ws"></option>
</select>
What's happening here:
select
element to a javascript variable called worksheet
(which we will create later.)worksheets
. Later we will be populating this variable with the names of all the worksheets present on the dashboard.ws
) to the value of the select
element.Do this for each of the four inputs, making sure to tie different variables to each one and use the appropriate field or worksheet list for the options.
Next, we'll add a button to save the settings we're collecting from the inputs.
<button class="button is-link" @click="save">Save</button>
Again, here we are using Vue. This time to set the method that runs when the button is clicked (@click
). Finally, make sure to give the topmost container of your config page the ID of app
. This is what we'll use to link the Vue framework to our HTML. Take a look at the config.html page in the final project to see what this looks like.
Now that we have the inputs we need, we'll need to populate them with data from the dashboard. In the config.js file start by setting up a Vue app.
/* global tableau Vue */
let app = new Vue({
el: '#app',
data: {},
methods: {},
watch: {},
created: async function () {},
});
tableau
and Vue
libraries are available to use. Otherwise, you may get warnings that these libraries don't exist depending on your IDE.data
, is where we will store the variables for the inputs and information about the dashboard.methods
is where we'll put our functions for getting and saving data.watch
is what we'll use to listen for updates in the users' selections. For example, if you change which worksheet you want to use for the source, we'll need to reload the list of available fields on that worksheet.created
is where we will initialize the Tableau Extensions API and run our setup scripts.Let's go through each section in order, first up is data
. Here we want a variable for each input as well as variables to hold the list of available worksheets and fields on those worksheets.
sourceWorksheet: null,
filterWorksheet: null,
dimensionField: null,
measureField: null,
worksheets: [],
fields: []
Right now these are all null or empty and will be filled as the configuration page loads and the user makes selections.
Let's move on to the methods
. First, we'll set up the save function which will save the above values to the workbook settings when we click Save.
save: async function() {
tableau.extensions.settings.set("sourceWorksheet", this.sourceWorksheet);
tableau.extensions.settings.set("filterWorksheet", this.filterWorksheet);
tableau.extensions.settings.set("dimensionField", this.dimensionField);
tableau.extensions.settings.set("measureField", this.measureField);
await tableau.extensions.settings.saveAsync();
tableau.extensions.ui.closeDialog("");
},
When we run this function, it will look at the data variables from above and save them to settings of the same name. I highly recommend keeping variable names similar to your setting names to help keep track of them! Additionally, once the settings are saved, this will close the dialog pop-up box for configuration. To learn more about the Tableau Extensions API take a look at the documentation.
Next, let's add the functions we'll need to get data from the dashboard. First up is the method to get worksheets:
getWorksheets: function() {
const worksheets = tableau.extensions.dashboardContent.dashboard.worksheets;
this.worksheets = [...worksheets.map((w) => w.name)];
const settings = tableau.extensions.settings.getAll();
this.sourceWorksheet = worksheets.find((w) => w.name === settings.sourceWorksheet) ? settings.sourceWorksheet : '';
this.filterWorksheet = worksheets.find((w) => w.name === settings.filterWorksheet) ? settings.filterWorksheet : '';
}
Let's break it down:
worksheets
data variable. Here we're using the spread operator and map functions to help.sourceWorksheet
and filterWorksheet
variables accordingly. Here we're using the find function to look for the matching worksheet along with the conditional operator.Next, we'll get the fields for the selected sourceWorksheet
.
getFields: async function (worksheetName) {
const worksheets = tableau.extensions.dashboardContent.dashboard.worksheets;
const worksheet = worksheets.find((w) => w.name === worksheetName);
const data = await worksheet.getSummaryDataAsync();
this.fields = [...data.columns.map((column) => column.fieldName)];
const settings = tableau.extensions.settings.getAll();
this.dimensionField = data.columns.find((c) => c.fieldName === settings.dimensionField) ? settings.dimensionField : '';
this.measureField = data.columns.find((c) => c.fieldName === settings.measureField) ? settings.measureField : '';
}
fields
data variable.That completes the methods
section, next, let's fill in the watch
section. The functions in this section run whenever the variable of the same is changed. This is how we'll trigger the getFields()
method to run each time the worksheet changes.
sourceWorksheet: function(worksheetName) {
this.getFields(worksheetName);
}
All we're doing here is getting the new worksheet name that was just selected and passing it to the getFields function to do the updating.
The last section, created
, is where we will initialize the API and run the getWorksheets()
method to populate the worksheet inputs when you first open the config window.
await tableau.extensions.initializeDialogAsync();
this.getWorksheets();
We're now finished with the config.js file. Before we can open it in Tableau we need to tell Tableau how to find it and how to open it in our script.js file. (Remember to mark the global variables tableau
and Chart
that we'll use later.)
/* global tableau Chart */
tableau.extensions.initializeAsync({ configure: configure }).then(() => {
// ...
});
Before we can use the API we'll initialize it, notice that we are passing in a configure function in the options, here's what the configure function looks like:
async function configure() {
try {
const url = `${window.location.origin}/config.html`;
await tableau.extensions.ui.displayDialogAsync(url, '', {
width: 500,
height: 600,
});
// ... more to come here ...
} catch (error) {
switch (error.errorCode) {
case tableau.ErrorCodes.DialogClosedByUser:
console.log('Dialog was closed by user.');
break;
default:
console.error(error.message);
}
}
}
Here we're loading our config.html page in a pop-up window that is 500 x 600 pixels. Take note that the domain used for the config page needs to be the same as what is used for your index.html page or Tableau will not open it. Additionally, we've added some error handling if the configuration window is closed unexpectedly.
Save all your files and now bring a new extension with your trex into your dashboard. You should see a "Configure..." option in the context menu of your extension and clicking it will pop open the configuration page with all the inputs. Choose some options (make sure there is a worksheet on your dashboard first!) and hit Save. If you re-open the configure page you should see your selected worksheets and fields saved there!
Now that we know all the details that we'll need to get the dashboard data and create the chart, let's set up the canvas where it will be drawn. On the index.html page, bring in the Tableau Extensions API and the Chart.js library.
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.6.0/chart.min.js"></script>
<script src="https://extensions.tableauusercontent.com/resources/tableau.extensions.1.latest.min.js"></script>
Then let's set up a canvas element we can use to draw the chart as well as a containing div
to help with styling and positioning of the canvas. Within the <body>
tags we'll add the following:
<div class="canvas">
<canvas id="canvas"></canvas>
</div>
Finally, we can add some minimal styling on the page and container class to fill the space of the extension zone within the styles.css file:
@import 'https://cdn.jsdelivr.net/npm/[email protected]/css/bulma.min.css';
html,
body {
margin: 0;
padding: 0;
height: 100%;
font-family: Helvetica, Arial, sans-serif;
font-size: 16px;
}
.canvas {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
Now from here, all that remains is our script.js file.
In order to build the chart we'll need to get data from the dashboard, specifically from the worksheets and fields selected in the configuration. Let's create a getData()
function.
async function getData() {
const settings = tableau.extensions.settings.getAll();
const worksheets = tableau.extensions.dashboardContent.dashboard.worksheets;
const worksheet = worksheets.find((ws) => ws.name === settings.sourceWorksheet);
const dataTable = await worksheet.getSummaryDataAsync();
const dimensionFieldIndex = dataTable.columns.find(
(column) => column.fieldName === settings.dimensionField
).index;
const measureFieldIndex = dataTable.columns.find(
(column) => column.fieldName === settings.measureField
).index;
let data = [];
for (let row of dataTable.data) {
data.push({
label: row[dimensionFieldIndex].value,
value: row[measureFieldIndex].value,
});
}
data.sort((a, b) => (a.label > b.label ? 1 : -1));
drawChart(data);
}
Let's break this one down:
columns
object then we can use it to pull the right values.label
and value
.drawChart()
. Let's build that next!Now that we have the data let's pass it to Chart.js to plot a chart. Specifically, let's build a polar area chart.
function drawChart(vizData) {
let values = vizData.map((row) => row.value);
let labels = vizData.map((row) => row.label);
let ctx = document.getElementById('canvas').getContext('2d');
let data = {
datasets: [
{
data: values,
backgroundColor: '#4E79A7',
},
],
labels: labels,
};
let options = {
onClick: filter,
maintainAspectRatio: false,
};
let polarChart = new Chart(ctx, {
data: data,
type: 'polarArea',
options: options,
});
}
getData()
into their own separate arrays.ctx
filter()
function next!)ctx
canvas with our data and options.For our last function, we'll tell Tableau what to do when a mark is selected in our Chart.js polar area chart.
function filter(event, item) {
const settings = tableau.extensions.settings.getAll();
const worksheets = tableau.extensions.dashboardContent.dashboard.worksheets;
const worksheet = worksheets.find((ws) => ws.name === settings.filterWorksheet);
if (item[0]) {
let index = item[0].index;
let label = event.chart.data.labels[index];
worksheet.applyFilterAsync(settings.dimensionField, [label], 'replace');
} else {
worksheet.clearFilterAsync(settings.dimensionField);
}
}
When something in Chart.js is selected it will pass information about the event as well as what was selected and we can use this to filter one of the worksheets on the dashboard.
To make this all work, we need to make sure the getData()
function is run when we first initialize the extension and after the configuration is set. This way whenever something changes in the config or if the extension is reloaded we re-render the chart with the correct settings. Add it in the initialization...
tableau.extensions.initializeAsync({ configure: configure }).then(() => {
getData();
});
... and in the configuration function.
await tableau.extensions.ui.displayDialogAsync(url, '', {
width: 500,
height: 600,
});
getData();
At this point, you should be all set to use your extension to create a polar area chart! Download this sample workbook to try it out! When you first load the extension it will be blank. Next, configure it to use the right worksheets and fields. Finally hit save and voila, there's your custom chart!
This tutorial is really just scratching the surface of all the things you can do. Not only are there plenty of other chart types but you can add more enhancements to the extension to make it more user-friendly or complex. Here are some ideas if you want to try your hand at taking this tutorial extension to the next level:
You can find the full code for this tutorial in the glitch below. Feel free to remix it for your own personal use and if you enjoyed this tutorial subscribe below to be notified of my latest posts!
Have an idea for a tutorial or resource? Drop me a request on the right!
Sign up to never miss a post!
Sign up to never miss a post!
Want me to cover a specific topic? Let me know!
Leave a comment