Fully Custom Tableau Integration

Replace DOM elements with fully-custom dashboards

Say, for example, you have a web-based dashboard to serve people information, but want an easy way to replace part or all of that dashboard with up-to-date information from external data sources or AI models. In this use case, we replace part of a Tableau dashboard with a UI that is populated with data being pulled at runtime from S3. Because this information will be integrated back into a preexisting Tableau dashboard, AI Squared rendering components might not be suitable to this use case. Therefore, we will use a completely custom rendering step.

Here's the output for this use case, which is a table (denoted my the green arrow) rendered over a preexisting element in a Tableau dashboard:

Harvesting

For this use case, we do not need to harvest any information from the webpage - this is purely pulling information and overlaying it. Therefore, we can leave the Harvesting step empty here.

Analytics

Here we're pulling data from AWS S3, so we'll be using the ReverseML class to point to the data source we want to reverse-ETL into this Tableau visualization.

Pre- and Post-Processing

As this is just an information push from a data source, there is no need to do any pre or post processing of any information being sent to or received from the analytic. Therefore, these steps can be left empty.

Rendering

Because we need to integrate into a preexisting dashboard, the out-of-the-box AI Squared rendering components may not be applicable to this use case. Because of this, we use the custom rendering class in the AI Squared platform editor to allow you to associate Javascript, HTML, and CSS with the information you are integrating. We need to perform this operation in 2 steps.

Create a Rendering Container

The first step is to create a "canvas" to render our custom UI on top of. We use the Container Rendering component to create an iframe within the DOM that we overlay the custom rendering on.

Here we are injecting this container relative to the DOM content, which works well for Tableau or other integrations into dashboards.

Custom Rendering

To create the custom dashboard element, there are input windows for JavaScript, HTML, and CSS within the platform UI.

Here is the code we use for this example:

HTML

<div id='custom-air-container'>
    <div class="container mainContainer">
        <div class="container tableContainer">
            <div class="row">
                <table class="table" cellspacing="0.25" cellpadding="0.25">
                    <thead id="tableHeader">
                        <tr>
                            <th colspan="1" class="majorHeader" id="clientData" style="border: none !important; color: #9F0015; font-weight: bold; background: white !important; padding-right: 5px; height: 52px;">Client Data</th>
                            <th colspan="2" class="majorHeader" style="border: 3px solid black; font-size: 11px; font-weight: 400; text-align: center; background: white !important;  height: 52px;">CXI</th>
                            <th colspan="6" class="majorHeader" style="border: 3px solid black; font-size: 11px; font-weight: 400; text-align: center; background: white !important;  height: 52px;">CSAT Digital</th>
                            <th colspan="6" class="majorHeader" style="border: 3px solid black; font-size: 11px; font-weight: 400; text-align: center; background: white !important;  height: 52px;">CSAT Omni</th>
                            <th colspan="7" class="majorHeader" style="border: 3px solid black; font-size: 11px; font-weight: 400; text-align: center; background: white !important;  height: 52px;">NPS</th>
                        </tr>
                    </thead>
                    <tbody id="tableData">
                    </tbody>
                </table>
            </div>
        </div>
    </div>
</div> 

JavaScript

const renderedData = renderer.predictions;
const processedData = processRenderedData(renderedData);
const headerNames = processHeaderNames(renderedData);
console.log(renderedData);
console.log(processedData);
console.log(headerNames);
let totalArray=['Grand Total'];
populateTable();

function processRenderedData(data){
    let result = [];
    for(let i=0; i<data.length; i++){
        let array = data[i];
        let object = {};

        for(let j=0; j<array.entries.length; j++){
            let dataArray = array.entries[j];

            let keyName = dataArray.name;
            let value = dataArray.value;

            object[keyName] = value;
        };
        result.push(object);
    };

    return result;
};

function processHeaderNames(data){
    let headerArray = [];
    for(let i=0; i<1; i++){
        let array = data[i];
        let object = {};

        for(let j=0; j<array.entries.length; j++){
            let dataArray = array.entries[j];

            let keyName = dataArray.name;
            let value = dataArray.value;

            if(i===0){
                headerArray.push(keyName);
            };
        };
    };

    return headerArray;
};

function populateAverageRow(key) {
    if(key === 'client Id') return;

    let sum = 0;
    for(i=0; i<processedData.length; i++){
        sum = (sum + Number(processedData[i][key]))
    }
    
    let average = sum / processedData.length;

    totalArray.push(Math.round(average));
};

function populateTable() {

    let tableHeaderElem = document.getElementById('tableHeader');
    let tableDataElem = document.getElementById('tableData');

    let tableHeaderRow = document.createElement('tr');

    for (let i=0; i<headerNames.length; i++){

        if(i > 21) break;

        let field = headerNames[i];
        
        let headerElem = document.createElement('th');
        headerElem.className = 'headerElem';

        if(i>0){
            headerElem.className += ' verticalTableHeader';
        }

        if(i===0){
            headerElem.style.verticalAlign = "bottom";
        };
        
        
        headerElem.style.fontSize = '12px';
        let headerText = field;

        headerElem.innerText = headerText;
        
        tableHeaderElem.appendChild(headerElem);
        tableHeaderRow.appendChild(headerElem);
        tableHeaderElem.appendChild(tableHeaderRow);

        populateAverageRow(field);
    };

    let totalRow = document.createElement('tr');

    for(let i=0; i<totalArray.length; i++){
        let value = totalArray[i];
        let dataElem = document.createElement('td');

        dataElem.className = 'dataElem';
        dataElem.style.fontSize = '12px';

        if(i > 0) {
            dataElem.style.textAlign = 'center';
        };

        let data = value;

        dataElem.innerText = data;
        tableDataElem.appendChild(dataElem);
        totalRow.appendChild(dataElem);
        tableDataElem.appendChild(totalRow);
    };

    for(let i=0; i<processedData.length; i++){
        let tableRow = document.createElement('tr');

        if(i % 2 === 0){
            tableRow.style.backgroundColor = "#f5f5f5"
        };

        //console.log(processedData[i]);
        let clientId = `Client Id: ` + processedData[i]['client Id'];
        let clientName = `Client Name: ` + processedData[i]['Client Name'];
        let callCount = `Call Count: ` + processedData[i]['Call Count'];
        let imwCount = `IMW Count: ` + processedData[i]['IMW Count'];
        let webexCount = `WebEx Count: ` + processedData[i]['WebEx Count'];

        let toolTip = 
        `
        ${clientId}
        ${clientName}
        ${callCount}
        ${imwCount}
        ${webexCount}
        `;
        
        for (let j=0; j<Object.keys(processedData[i]).length ; j++){

            let value = Object.values(processedData[i])[j];
            if(j > 21) break;

            let dataElem = document.createElement('td');

            dataElem.style.fontSize = '12px';
            if(j > 0) {
                dataElem.style.textAlign = 'center';
            };
            
            dataElem.title = toolTip;
            let data = value;
            
            dataElem.innerText = data;
            tableDataElem.appendChild(dataElem);
            tableRow.appendChild(dataElem);
            tableDataElem.appendChild(tableRow);
        }
    }
};

CSS

.verticalTableHeader {
    writing-mode: vertical-lr;
    min-width: 50px; /* for firefox */
    transform: rotate(180deg);
    max-height: 100px;
    text-align: center  !important;
};


#clientData {
    color: #E15759;
    font-weight: bold;
};



table {
    /* overflow: hidden;  add this */
    table-layout: fixed;
    width: 1119;
    
    
};

The JS interpreter used by AI Squared is the Sval interpreter (https://github.com/Siubaak/sval), which goes up to ECMAscript 2019. Therefore, it is flexible enough to support all operations you could wish to perform but some expressions (e.g. for each) are not handled and will result in an error.

Feedback

Here we are asking two simple questions of end users, which we handle using Simple Feedback feedback components:

And voila, you're up and running with this use case!

Last updated