Skip to content

Instantly share code, notes, and snippets.

@sdowsland
Last active February 12, 2016 14:30
Show Gist options
  • Save sdowsland/a674d0e3fd3206e6803c to your computer and use it in GitHub Desktop.
Save sdowsland/a674d0e3fd3206e6803c to your computer and use it in GitHub Desktop.

Revisions

  1. sdowsland renamed this gist Feb 12, 2016. 1 changed file with 0 additions and 0 deletions.
    File renamed without changes.
  2. sdowsland created this gist Feb 12, 2016.
    304 changes: 304 additions & 0 deletions tabl-connect.html
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,304 @@
    <html>
    <head>
    <title>json Connector</title>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js" type="text/javascript"></script>
    <script src="https://public.tableau.com/javascripts/api/tableauwdc-1.1.0.js" type="text/javascript"></script>
    <script type="text/javascript">

    (function() {

    var myConnector = tableau.makeConnector();

    myConnector.init = function () {
    tableau.connectionName = 'JSON data';
    tableau.initCallback();
    };

    myConnector.getColumnHeaders = function() {
    _retrieveJsonData(function (tableData) {
    var headers = tableData.headers;
    var fieldNames = [];
    var fieldTypes = [];

    for (var fieldName in headers) {
    if (headers.hasOwnProperty(fieldName)) {
    fieldNames.push(fieldName);
    fieldTypes.push(headers[fieldName]);
    }
    }
    tableau.headersCallback(fieldNames, fieldTypes); // tell tableau about the fields and their types
    });
    };

    myConnector.getTableData = function (lastRecordToken) {
    _retrieveJsonData(function (tableData) {
    var rowData = tableData.rowData;
    tableau.dataCallback(rowData, rowData.length.toString(), false);
    });
    };

    tableau.registerConnector(myConnector);
    })();

    function _retrieveJsonData(retrieveDataCallback) {
    if (!window.cachedTableData) {
    var conData = JSON.parse(tableau.connectionData);
    var jsonString = conData.jsonString;
    if (conData.jsonUrl) {
    var successCallback = function(data)
    {
    window.cachedTableData = _jsToTable(data);
    retrieveDataCallback(window.cachedTableData);
    };
    // Go get the json data
    // This is the first, and most basic, of many attempts to get the data.
    // We make different attempts that use different approaches, because if
    // the server we are requesting data from has not enabled CORS, then the
    // request will fail. In that case we try different proxies to try to get
    // around the same origin policy. This is not as clean as it could be, but
    // for a general purpose connector, we want to make every attempt we can
    // to pull in the data
    _basicAjaxRequest1(conData.jsonUrl, successCallback);
    return;
    }
    try {
    window.cachedTableData = _jsToTable(JSON.parse(conData.jsonString));
    }
    catch (e) {
    tableau.abortWithError("unable to parse json data");
    return;
    }
    }
    retrieveDataCallback(window.cachedTableData);
    }

    // There are a lot of ways to handle URLS. Sometimes we'll need workarounds for CORS. These
    // methods chain together a series of attempts to get the data at the given url

    function _ajaxRequestHelper(url, successCallback, conUrl, nextFunction, specialSuccessCallback){
    specialSuccessCallback = specialSuccessCallback || successCallback;
    var xhr = $.ajax({
    url: conUrl,
    dataType: 'json',
    success: specialSuccessCallback,
    error: function()
    {
    nextFunction(url, successCallback);
    }
    });
    }

    // try the straightforward request
    function _basicAjaxRequest1(url, successCallback){
    _ajaxRequestHelper(url, successCallback, url, _corsioProxyAjaxRequest2);
    }

    // try to use cors.io as a proxy
    function _corsioProxyAjaxRequest2(url, successCallback){
    var corsUrl = "http://cors.io/?u=" + url;
    _ajaxRequestHelper(url, successCallback, corsUrl, _yqlProxyAjaxRequest3);
    }

    // try to use yql as a proxy
    function _yqlProxyAjaxRequest3(url, successCallback){
    var yqlQueryBase = "http://query.yahooapis.com/v1/public/yql?q=";
    var query = "select * from html where url='" + url + "'";
    var restOfQueryString = "&format=json";
    var yqlUrl = yqlQueryBase + encodeURIComponent(query) + restOfQueryString;
    var specialSuccessCallback = function(data) {
    if (data.query.count == 0) {
    _jsonpAjax4(url, successCallback);
    } else {
    successCallback(data);
    }
    }
    _ajaxRequestHelper(url, successCallback, yqlUrl, _jsonpAjax4, specialSuccessCallback);
    }

    // try jsonP as a final approach?
    function _jsonpAjax4(url, successCallback){
    var xhr = $.ajax({
    url: url + "?callback=?",
    dataType: 'jsonp',
    success: successCallback,
    error: function()
    {
    _giveUpOnUrl9(url, successCallback);
    }
    });
    }

    function _giveUpOnUrl9(url, successCallback) {
    tableau.abortWithError("Could not load url: " + url);
    }

    // Takes a hierarchical javascript object and tries to turn it into a table
    // Returns an object with headers and the row level data
    function _jsToTable(objectBlob) {
    var rowData = _flattenData(objectBlob);
    var headers = _extractHeaders(rowData);
    return {"headers":headers, "rowData":rowData};
    }

    // Given an object:
    // - finds the longest array in the object
    // - flattens each element in that array so it is a single object with many properties
    // If there is no array that is a descendent of the original object, this wraps
    // the input in a single element array.
    function _flattenData(objectBlob) {
    // first find the longest array
    var longestArray = _findLongestArray(objectBlob, []);
    if (!longestArray || longestArray.length == 0) {
    // if no array found, just wrap the entire object blob in an array
    longestArray = [objectBlob];
    }
    for (var ii = 0; ii < longestArray.length; ++ii) {
    _flattenObject(longestArray[ii]);
    }
    return longestArray;
    }

    // Given an object with hierarchical properties, flattens it so all the properties
    // sit on the base object.
    function _flattenObject(obj) {
    for (var key in obj) {
    if (obj.hasOwnProperty(key) && typeof obj[key] == 'object') {
    var subObj = obj[key];
    _flattenObject(subObj);
    for (var k in subObj) {
    if (subObj.hasOwnProperty(k)) {
    obj[key + '_' + k] = subObj[k];
    }
    }
    delete obj[key];
    }
    }
    }

    // Finds the longest array that is a descendent of the given object
    function _findLongestArray(obj, bestSoFar) {
    if (!obj) {
    // skip null/undefined objects
    return bestSoFar;
    }

    // if an array, just return the longer one
    if (obj.constructor === Array) {
    // I think I can simplify this line to
    // return obj;
    // and trust that the caller will deal with taking the longer array
    return (obj.length > bestSoFar.length) ? obj : bestSoFar;
    }
    if (typeof obj != "object") {
    return bestSoFar;
    }
    for (var key in obj) {
    if (obj.hasOwnProperty(key)) {
    var subBest = _findLongestArray(obj[key], bestSoFar);
    if (subBest.length > bestSoFar.length) {
    bestSoFar = subBest;
    }
    }
    }
    return bestSoFar;
    }

    // Given an array of js objects, returns a map from data column name to data type
    function _extractHeaders(rowData) {
    var toRet = {};
    for (var row = 0; row < rowData.length; ++row) {
    var rowLine = rowData[row];
    for (var key in rowLine) {
    if (rowLine.hasOwnProperty(key)) {
    if (!(key in toRet)) {
    toRet[key] = _determineType(rowLine[key]);
    }
    }
    }
    }
    return toRet;
    }

    // Given a primitive, tries to make a guess at the data type of the input
    function _determineType(primitive) {
    // possible types: 'float', 'date', 'datetime', 'bool', 'string', 'int'
    if (parseInt(primitive) == primitive) return 'int';
    if (parseFloat(primitive) == primitive) return 'float';
    if (isFinite(new Date(primitive).getTime())) return 'datetime';
    return 'string';
    }

    function _submitToJsonToTableau(jsonString, jsonUrl) {
    var conData = {"jsonString" : jsonString, "jsonUrl": jsonUrl};
    tableau.connectionData = JSON.stringify(conData);
    tableau.submit();
    }

    $(document).ready(function(){
    var cancel = function (e) {
    e.stopPropagation();
    e.preventDefault();
    }
    $("#inputForm").submit(function(e) { // This event fires when a button is clicked
    // Since we use a form for input, make sure to stop the default form behavior
    cancel(e);
    var jsonString = $('textarea[name=jsonText]')[0].value.trim();
    var jsonUrl = $('input[name=jsonUrl]')[0].value.trim();
    _submitToJsonToTableau(jsonString, jsonUrl);
    });
    var ddHandler = $("#dragandrophandler");
    ddHandler.on('dragenter', function (e)
    {
    cancel(e);
    $(this).css('border', '2px solid #0B85A1');
    }).on('dragover', cancel)
    .on('drop', function (e)
    {
    $(this).css('border', '2px dashed #0B85A1');
    e.preventDefault();
    var files = e.originalEvent.dataTransfer.files;
    var file = files[0];
    var reader = new FileReader();
    reader.onload = function(e) { _submitToJsonToTableau(reader.result); };
    reader.readAsText(file);
    });

    $(document).on('dragenter', cancel)
    .on('drop', cancel)
    .on('dragover', function (e)
    {
    cancel(e);
    ddHandler.css('border', '2px dashed #0B85A1');
    });
    });

    </script>

    <style>
    #dragandrophandler {
    border:1px dashed #999;
    width:300px;
    color:#333;
    text-align:left;vertical-align:middle;
    padding:10px 10px 10 10px;
    margin:10px;
    font-size:150%;
    }
    </style>
    </head>
    <body>

    <form id="inputForm" action="">
    Enter a URL for JSON data:
    <input type="text" name="jsonUrl" size="50" />
    <br>
    <div id="dragandrophandler">Or Drag & Drop Files Here</div>
    <br>
    Or paste JSON data below
    <br>
    <textarea name="jsonText" rows="10" cols="70"/></textarea>
    <input type="submit" value="Submit">
    </form>

    </body>
    </html>