UNPKG

gojs

Version:

Interactive diagrams, charts, and graphs, such as trees, flowcharts, orgcharts, UML, BPMN, or business diagrams

592 lines (505 loc) 20.1 kB
/* * Functionality in this file pertains to how GoTimeline imports data from outside sources (Google Drive, Local Drive) * This includes auth flows as well as the actual formatting and importing of data */ replaceNodeData = true; // local disk csv file data import stuff function readSingleFile(e) { var file = e.target.files[0]; if (!file) { return; } var reader = new FileReader(); reader.onload = function(e) { var contents = e.target.result; getImportDataType(file, categories); }; reader.readAsText(file); } // Google Drive data import stuff var CLIENT_ID = "16225373139-n24vtg7konuetna3ofbmfcaj2infhgmg.apps.googleusercontent.com"; var API_KEY = "AIzaSyCeMqYjdrskXIgtv35kyet8ji2m3_ilydY"; var DISCOVERY_DOCS = ["https://www.googleapis.com/discovery/v1/apis/drive/v3/rest"]; var SCOPES = 'https://www.googleapis.com/auth/drive'; gPicker = null; gOAuthToken = null; /** * On load, called to load the auth2 library and API client library. */ function authorizeGoogleUserForDataImport() { function auth() { gapi.auth.authorize({ 'client_id': CLIENT_ID, 'scope': SCOPES, 'immediate': false },function (authResult) { if (window['google']) gPicker = window['google']['picker']; // used for importing google drive sheets for data gOAuthToken = authResult.access_token if (authResult && !authResult.error) { createGoogleSheetPicker(getImportDataType); } }); } gapi.load('client:auth', auth); gapi.load('picker', {}); } /** * Initializes the API client library and sets up sign-in state * listeners. */ function initClient() { gapi.client.init({ apiKey: API_KEY, clientId: CLIENT_ID, discoveryDocs: DISCOVERY_DOCS, scope: SCOPES }).then(function () { // Listen for sign-in state changes. gapi.auth2.getAuthInstance().isSignedIn.listen(updateSigninStatus); // Handle the initial sign-in state. updateSigninStatus(gapi.auth2.getAuthInstance().isSignedIn.get()); }); } /* * Ask user if what type(s) of Nodes the data file to import is for * @param f file to import * @param categories -- an Array of possible Node template categories to choose from */ function getImportDataType (f, categories) { // remove any pre-existing data ordering div var dom = document.getElementById("ge-data-type-div"); if (dom) { document.body.removeChild(dom); } var dataTypeMenu = document.createElement("div"); dataTypeMenu.id = "ge-data-type-div"; dataTypeMenu.className = "ge-menu"; var titleDiv = document.createElement("div"); titleDiv.id = "ge-data-type-title-div"; var title = document.createElement("h4"); var fname = (f.docs == undefined) ? f.name : f.docs[0].name; title.innerText = fname; title.id = "ge-data-type-title"; titleDiv.appendChild(title); var desc = document.createElement("i"); desc.innerHTML = "What Node type is this data for?"; titleDiv.appendChild(desc); dataTypeMenu.appendChild(titleDiv); for (var i = 0; i < categories.length; i++) { var cat = categories[i]; var b = document.createElement("button"); b.className = "ge-button"; b.innerText = cat + " Nodes"; b.data = cat; if (cat === "") b.innerText = "Default"; b.onclick = function () { buildDataOrdering(f, this.data); document.body.removeChild(dataTypeMenu); } dataTypeMenu.appendChild(b); } var cb = document.createElement("button"); cb.innerText = "Cancel"; cb.className = "ge-button"; cb.onclick = function () { document.body.removeChild(dataTypeMenu); } document.body.appendChild(dataTypeMenu); } /* * get info from user on how their data is ordered in the file they want to import data from * @param f The file used for data import * @param cat The category of Node this data will be used to create */ function buildDataOrdering(f, cat) { // this is a JavaScript object that holds data props supported by this Node category var dataProps = nodeGenerationMap.get(cat).dataProps; // reset dataOrdering -- this is used so we know what data corresponds with what column when we import // kvp's are <col><data property name> var dataOrdering = new go.Map(); // is "f" some data passed from the Google Picker API? if (f.docs != undefined) { f = f.docs[0]; } if (f.name == undefined) return; // remove any pre-existing data ordering div var dom = document.getElementById("ge-data-ordering-div"); if (dom) { document.body.removeChild(dom); } var dataOrderingMenu = document.createElement("div"); dataOrderingMenu.id = "ge-data-ordering-div"; dataOrderingMenu.className = "ge-menu"; var titleDiv = document.createElement("div"); titleDiv.id = "ge-data-ordering-title-div"; var title = document.createElement("h4"); title.innerText = f.name; title.id = "ge-data-ordering-title"; titleDiv.appendChild(title); var desc = document.createElement("i"); desc.innerHTML = "Tell us how your data is organized, and we'll do the rest. <hr/>"; titleDiv.appendChild(desc); dataOrderingMenu.appendChild(titleDiv); var datasDiv = document.createElement("div"); datasDiv.id = "ge-datas-div"; // <select> elements var i = 1; for (let index in dataProps) { var div = document.createElement("div"); div.className = "ge-data-option-div"; var p2 = document.createElement("span"); p2.innerText = "What data is in column " + i + "?"; div.appendChild(p2); var s = document.createElement("select"); s.id = i-1; for (let it in dataProps) { var option = document.createElement("option"); option.value = dataProps[it].name; option.innerText = dataProps[it].name; s.appendChild(option); } s.value = dataProps[i-1].name; dataOrdering.set(i-1, s.value); // update fileDataOrdering map if <select> element changes s.onchange = function () { dataOrdering.set(this.id,this.value) } div.appendChild(s); datasDiv.appendChild(div); i++; } dataOrderingMenu.appendChild(datasDiv); var checkboxDiv = document.createElement("div"); checkboxDiv.id = "ge-replace-event-data-checkbox-div"; // checkbox to handle whether or not to replace all model data with imported data var checkbox = document.createElement("input"); checkbox.type = "checkbox"; checkbox.checked = replaceNodeData; checkbox.className = "ge-checkbox"; checkbox.id = "ge-replace-event-data-checkbox"; checkbox.onchange = function () { replaceNodeData = this.checked; } var cbLabel = document.createElement("label"); cbLabel.id = "ge-replace-event-data-checkbox-label"; cbLabel.for = checkbox.id; cbLabel.innerText = "Replace existing node data?"; checkboxDiv.appendChild(checkbox); checkboxDiv.appendChild(cbLabel); dataOrderingMenu.appendChild(checkboxDiv); // load button - try to load the file // this will fail if the user has not given a unique data type for each column var lb = document.createElement("button"); lb.innerText = "Load Data"; lb.className = "ge-button"; lb.onclick = function () { var fileInput = document.getElementById('file-input'); fileInput.value = ""; var selectValues = []; var divs = dataOrderingMenu.getElementsByTagName("div"); for (var j = 0; j < divs.length; j++) { var div = divs[j]; if (div.className != "ge-data-option-div") continue; var selectVal = div.getElementsByTagName("select")[0].value; if (selectValues.indexOf(selectVal) != -1 ) { storageManager.showMessage("Each column must hold different data"); return; } else { selectValues.push(selectVal); } } // if we got to here, all select values are unique document.body.removeChild(dataOrderingMenu); // it's a google drive file if (f.parentId != undefined) { importGoogleDriveData(f, cat, dataOrdering); } // it's a local file on disk else { var reader = new FileReader(); reader.onload = function(e) { var contents = e.target.result; importDataFromContents(f, contents, cat, dataOrdering); }; reader.readAsText(f); } } // end onclick for load button // choose a different file button var fb = document.createElement("button"); fb.innerText = "Choose different file"; fb.className = "ge-button"; fb.onclick = function () { var fileInput = document.getElementById('file-input'); fileInput.value = ""; document.body.removeChild(dataOrderingMenu); document.getElementById('file-input').click(); } // cancel button var cb = document.createElement("button"); cb.innerText = "Cancel"; cb.className = "ge-button"; cb.onclick = function () { var fileInput = document.getElementById('file-input'); fileInput.value = ""; document.body.removeChild(dataOrderingMenu); } dataOrderingMenu.appendChild(lb); dataOrderingMenu.appendChild(fb); dataOrderingMenu.appendChild(cb); document.body.appendChild(dataOrderingMenu); } // Import data from Google Drive and construct a diagram with it function importGoogleDriveData(f, cat, dataOrdering) { gapi.client.request({ 'path': '/drive/v3/files/' + f.id + "/export", 'params': { 'mimeType': 'text/csv' }, 'method': 'GET', callback: function (p1,req) { var json = JSON.parse(req); var data = json.gapiRequest.data.body; importDataFromContents(f, data, cat, dataOrdering); } // end callback }); } // generic -- loads csv data into diagram data function importDataFromContents(f, fileContents, cat, dataOrdering) { var nodeDataArray = []; var nodeGenerationObj = nodeGenerationMap.get(cat); // load in all events or ranges in the spreadsheet var lines = fileContents.split('\n'); for (var i = 0; i < lines.length; i++) { var line = lines[i]; if (line === "") break; var tokens = line.split(","); // delete blank tokens for (var j = 0; j < tokens.length; j++) { if (tokens[j] == "") { tokens.splice(j,1); j--; } } // sweep through tokens for data based on dataOrdering var nodeData = nodeGenerationObj.defaultNodeData; for (var k = 0; k < dataOrdering.size; k++) { var prop = dataOrdering.get(k); // data of type dataType is in column k var dataPropType = null; for (let m in nodeGenerationObj.dataProps) { if (nodeGenerationObj.dataProps[m].name === prop) { dataPropType = nodeGenerationObj.dataProps[m].type; } } nodeData[prop] = tokens[k]; if (dataPropType === "boolean") { var str = tokens[k].replace(/\s/g, ''); if (str === "true") nodeData[prop] = true; if (str === "false") nodeData[prop] = false; else nodeData[prop] = true; } if (dataPropType === "date") { nodeData[prop] = makeLocalDate(tokens[k]); } } var nd = {}; for (let p in nodeData) { nd[p] = nodeData[p]; } nd["category"] = cat; nodeDataArray.push(nd); nodeData = null; } // end line iteration if (replaceNodeData) { myDiagram.model.nodeDataArray = []; myDiagram.model.linkDataArray = []; } for (let i in nodeDataArray) { var nd = nodeDataArray[i]; var preds = nodeGenerationObj.predicates !== undefined ? nodeGenerationObj.predicates : null; var postCreateFunction = nodeGenerationObj.postCreateFunction !== undefined ? nodeGenerationObj.postCreateFunction : null; addNodeToDiagram(nd, preds, postCreateFunction); } } // @param cb callback function -- what to do with the response from the Picker -- the picked file function createGoogleSheetPicker (cb) { // (appId is just the first number of clientId before '-') let appId = "16225373139"; // GoCloudStorage app Id var view = new gPicker.View(gPicker.ViewId.DOCS); view.setMimeTypes("application/vnd.google-apps.spreadsheet"); var picker = new gPicker.PickerBuilder() .enableFeature(gPicker.Feature.NAV_HIDDEN) .enableFeature(gPicker.Feature.MULTISELECT_ENABLED) .setAppId(appId) .setOrigin(window.location.protocol + '//' + window.location.host) .setOAuthToken(gOAuthToken) .addView(view) .setDeveloperKey("AIzaSyDBj43lBLpYMMVKw4aN_pvuRg7_XMVGf18") // same picker api key as GoCloudStorage Google App .setCallback(function (args) { // only ask if range or event node data if a file is picked if (args.action === "picked") { cb(args, categories); } }) .build(); picker.setVisible(true); } // CREATE Nodes DYNAMICALLY /* * Create a Node of a given category using a UI * @param category -- optional? * @param dataProps -- an array of the data properties the node may have * Must be an Array of JavaScript objects. Each object must have a "name" and "type" property * @param defaultNodeData optional -- the default data for a Node of this category * @param predicates optional -- an Array of functions that act as predicates to determine whether this Node can be added * If any of these predicates returns false, the Node will not be created. * You must format your predicate functions to take a JavaScript object as a parameter * that represents the proposed data for the Node about to be created * @param postCreateFunction optional -- performed after the Node is created. * Perhaps you need to run some clean up or other checks -- if so, do them here. This function may take the newly created node as a parameter */ function createNode(category) { //if (!defaultNodeData) defaultNodeData = {}; if (!category) category = ""; var nodeGenerationObj = nodeGenerationMap.get(category); if (!nodeGenerationObj) throw new Error("nodeGenerationMap not defined at key " + category); var dataProps = nodeGenerationObj.dataProps; var defaultNodeData = nodeGenerationObj.defaultNodeData === undefined ? {} : nodeGenerationObj.defaultNodeData; var predicates = nodeGenerationObj.predicates === undefined ? null : nodeGenerationObj.predicates; var postCreateFunction = nodeGenerationObj.postCreateFunction === undefined ? null : nodeGenerationObj.postCreateFunction; // remove any preexisting create window var d = document.getElementById("ge-build-node-div"); if (d !== null && d !== undefined) { document.body.removeChild(d); } var buildNodeMenu = document.createElement("div"); buildNodeMenu.id = "ge-build-node-div"; buildNodeMenu.className = "ge-menu"; var titleDiv = document.createElement("div"); titleDiv.className = "ge-handle"; titleDiv.id = "ge-build-node-title-div"; var title = document.createElement("h4"); title.innerText = "Build " + category + "Node"; title.id = "ge-build-node-title"; titleDiv.appendChild(title); buildNodeMenu.appendChild(titleDiv); var desc = document.createElement("i"); desc.innerHTML = "Tell us all about this " + category + " Node."; buildNodeMenu.appendChild(desc); var dataPropertiesDiv = document.createElement("div"); dataPropertiesDiv.className = "ge-scrollable"; function addDataPropertyDiv(index) { var propertyDiv = document.createElement("div"); propertyDiv.className = "ge-build-node-property-div"; var dataPropName = dataProps[index].name; var dataPropType = dataProps[index].type; var dataPropDefaultVal = dataProps[index].value; if (dataPropDefaultVal == undefined) dataPropDefaultVal = ""; // label for input var label = document.createElement("option"); label.for = "ge-create-node-data-"+index; label.innerText = dataPropName; var input = document.createElement("input"); input.data = dataPropType; // this should change if the <select> tag associated with this changes input.id = "ge-create-node-data-"+index; switch (dataPropType) { case "date": input.type = "date"; break; case "boolean": input.type = "checkbox"; break; case "color": input.type = "color"; break; } if (input.type === "checkbox") { if (dataPropDefaultVal !== true && dataPropDefaultVal !== false) input.checked = true; else input.checked = dataPropDefaultVal; } input.value = dataPropDefaultVal; input.onkeyup = function(e){ if (e.keyCode == 13) { // enter key var crb = document.getElementById("ge-build-node-button"); crb.click(); } }; propertyDiv.appendChild(input); propertyDiv.appendChild(label); dataPropertiesDiv.appendChild(propertyDiv); } for (let i in dataProps) { addDataPropertyDiv(i); } buildNodeMenu.appendChild(dataPropertiesDiv); // create button var crb = document.createElement("button"); crb.id = "ge-build-node-button"; crb.innerText = "Create Node"; crb.className = "ge-button"; crb.onclick = function () { // build node data var dataPropertiesDivs = dataPropertiesDiv.getElementsByTagName("div"); function makeNodeDataCopy(nodeData) { var copy = {}; for (let prop in nodeData) { copy[prop] = nodeData[prop]; } return copy; } var nodeData = makeNodeDataCopy(defaultNodeData); nodeData["category"] = category; // iterate over each data property div, building kvp for a node's data for (var i = 0; i < dataPropertiesDivs.length; i++) { var div = dataPropertiesDivs[i]; var input = div.getElementsByTagName("input")[0]; var key = dataProps[i].name; var val = input.value; if (input.type === "checkbox") val = input.checked; if (input.type === "date") val = makeLocalDate(val); nodeData[key] = val; } addNodeToDiagram(nodeData, predicates, postCreateFunction); document.body.removeChild(buildNodeMenu); } // cancel button var cb = document.createElement("button"); cb.innerText = "Cancel"; cb.className = "ge-button"; cb.onclick = function () { document.body.removeChild(buildNodeMenu); } buildNodeMenu.appendChild(crb); buildNodeMenu.appendChild(cb); document.body.appendChild(buildNodeMenu); refreshDraggableWindows(); } /* * Constructs a node from some node data and adds it to the diagram, as long as all predicates return true * Called by createNode, as well as during the import process * @param nodeData * @param predicates * @param postCreateFunction */ function addNodeToDiagram(nodeData, predicates, postCreateFunction) { // if any of the supplied predicates returns false, do not create this Node var doNotCreate = false; if (predicates) { for (let i in predicates) { var pred = predicates[i]; if (!pred(nodeData)) { doNotCreate = true; } } } if (!doNotCreate) { myDiagram.model.addNodeData(nodeData); var node = myDiagram.findNodeForData(nodeData); myDiagram.select(node); if (postCreateFunction) { postCreateFunction(node); } } } // end data interaction / node generation // return a Date with the correct timezone offset. accepts a formatted string. // Dunno if this is the best place for this but its very important for anything that uses Dates, like GoTimeline function makeLocalDate(dateStr) { d = new Date(dateStr); d.setMinutes(d.getMinutes() + d.getTimezoneOffset()); return d; }