Convert HTML web app table to PDF using Google Apps Script - google-apps-script

every one I have HTML web app where I have converted Spreadsheet to HTML table now I would like to convert this HTML web app Table to PDF. How can this be accomplished?
Here is my Code
index.html
<html>
<head>
<script type="text/javascript" src="https://www.google.com/jsapi"></script>
<script type="text/javascript">
google.load("visualization", '1', {packages:['table']});
google.setOnLoadCallback(drawTable);
function drawTable() {
var query = new google.visualization.Query(
'https://docs.google.com/spreadsheets/d/1w5kqFmt1yclKVDdasujf-
moxjX3QOVsyDyP7DIcF2u0/edit#gid=0');
query.send(handleQueryResponse);
}
function handleQueryResponse(response) {
if (response.isError()) {
alert('Error in query: ' + response.getMessage() + ' ' +
response.getDetailedMessage());
return;
}
var data = response.getDataTable();
var table = new
google.visualization.Table(document.getElementById('table_div'));
var options = {'title':'Bass Naming',
'width':'600',
'height':'400'};
table.draw(data, options);
var tabUri = table.getLinkUrl();
}
</script>
<title>Data from a Spreadsheet</title>
</head>
<body>
<div>My Sheet</div>
<div id="table_div"></div>
<div>
<h3> click here the below button to send the Table as PDF to mail :</h3>
<button id='mail' onclick="myFunction()">Send to Mail</button>
</div>
</body>
</html>
code.gs
function doGet(e) {
return HtmlService
.createTemplateFromFile("index")
.evaluate()
.setTitle("Charts for Rent Analyser")
.setSandboxMode(HtmlService.SandboxMode.IFRAME);
}

Add the below function in code.gs to mail the table as PDF attachment.
function exportAsPdf(submittedBy) {
var file = Drive.Files.get('1w5kqFmt1yclKVDdasujf-moxjX3QOVsyDyP7DIcF2u0');
var url = file.exportLinks['application/pdf'];
var token = ScriptApp.getOAuthToken();
var response = UrlFetchApp.fetch(url, {
headers: {
'Authorization': 'Bearer ' + token
}
});
MailApp.sendEmail({
to:Session.getActiveUser().getEmail(),
subject:"Charts for Rent Analyser",
htmlBody: "PFA",
attachments:[response.getBlob().setName("Charts for Rent Analyser.pdf")],
});
}
Also make sure you enable the Drive services from "Advanced Google Service" and also the Drive API from "Goolge API Console".
Refer the below link for detailed code.
Google App Script
Exec Link

Related

Upload file and past the link into active cell a spreadsheet

Trying to make a script to upload a file and paste the downloaded file's link into the active cell of a google spreadsheet.
After clicking "Upload" in the modal window, the file is not written to Google Drive and, accordingly, the link is not written to the cell
Code.gs
function onOpen() {
SpreadsheetApp.getUi()
.createMenu('File')
.addItem('Attach...', 'showForm')
.addToUi();
}
function showForm() {
var html = HtmlService.createHtmlOutputFromFile('index');
SpreadsheetApp.getUi().showModalDialog(html, 'Upload File');
}
function uploadFile(e) {
var newFileName = e.fileName;
var blob = e.file;
var upFile = DriveApp.getFolderById('*FolderID*').createFile(blob).setName(newFileName);
Logger.log(upFile);
var fileUrl = upFile.getUrl();
var formula = '=HYPERLINK("' + fileUrl + '","' + newFileName + '")';
SpreadsheetApp.getActiveRange().setFormula( formula );
return "Uploaded!";
}
index.html
<!DOCTYPE html>
<html>
<head>
<base target="_center">
<link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons1.css">
</head>
<body>
<form id="myForm" >
Select File: <input type="file" name="file" accept="*" /><br>
File name: <input type="text" name="fileName" /><br><br>
<input type="button" value="Upload" onclick="upload(this.parentNode);" />
</form>
<script>
window.onload=func1;
function func1() {
document.getElementById('myForm').addEventListener('submit', function(event) {
event.preventDefault();
});
}
function upload(obj){
google.script.run.withSuccessHandler(close).withFailureHandler(close).uploadFile(obj);
}
function close(e) {
console.log(e);
google.script.host.close();
}
</script>
</body>
</html>
Issue and workaround:
On December 9, 2021, the file object got to be able to be parsed from the Javascript side to the Google Apps Script side with V8 runtime. But, in this case, this can be used for only Web Apps. In the current stage, the sidebar and dialog cannot parse the file object on the Javascript side. Ref I think that this is the reason of your issue. So, in the current stage, it is required to send the file object as the string and the byte array for the sidebar and the dialog.
In this case, in order to achieve your goal using the current workaround, I would like to propose the following 2 patterns.
By the way, I think that in your Google Apps Script, an error occurs at SpreadsheetApp.getActiveCell().setFormula( formula );. Because SpreadsheetApp has no method of getActiveCell(). In this case, I think that getActiveRange() might be suitable.
Modified script:
In this modification, the file object is converted to the byte array, and the data is sent to Google Apps Script side.
Google Apps Script side:
function showForm() {
var html = HtmlService.createHtmlOutputFromFile('index');
SpreadsheetApp.getUi().showModalDialog(html, 'Upload File');
}
function uploadFile(e) {
var blob = Utilities.newBlob(...e);
var upFile = DriveApp.getFolderById('*FolderID*').createFile(blob).setName(e[2]);
Logger.log(upFile);
var fileUrl = upFile.getUrl();
var formula = '=HYPERLINK("' + fileUrl + '","' + e[2] + '")';
SpreadsheetApp.getActiveRange().setFormula(formula);
return "Uploaded!";
}
HTML & Javascript side:
<!DOCTYPE html>
<html>
<head>
<base target="_center">
<link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons1.css">
</head>
<body>
<form id="myForm">
Select File: <input type="file" name="file" accept="*" /><br>
File name: <input type="text" name="fileName" /><br><br>
<input type="button" value="Upload" onclick="upload(this.parentNode);" />
</form>
<script>
window.onload = func1;
function func1() {
document.getElementById('myForm').addEventListener('submit', function(event) {
event.preventDefault();
});
}
function upload(obj) {
const file = obj.file.files[0];
// --- For your additional request, I modified below script.
const extension = function(e) {
const temp = e.split(".");
return temp.length == 1 ? "" : temp.pop();
}(file.name);
const filename = `${obj.fileName.value}.${extension}`;
// ---
const fr = new FileReader();
fr.onload = e => {
const obj = [[...new Int8Array(e.target.result)], file.type, filename];
google.script.run.withSuccessHandler(close).withFailureHandler(close).uploadFile(obj);
};
fr.readAsArrayBuffer(file);
}
function close(e) {
console.log(e);
google.script.host.close();
}
</script>
</body>
</html>
Note:
In your script, I thought that the modified script might be a bit complicated. So, I posted the modified script as an answer.
Reference:
Related thread.
How do I convert jpg or png image to Blob via a Google Apps Script Web App?

Upload file to Google Team Drive from php or html form

I am considering using either php or html form with an upload button to upload a file to a Google Team Drive.
I am wondering if anyone has had success using the Google Picker API? If so, would you kindly share your solution.
This is the info I'm referencing on Google API
Thank you for your help!
After hours of trial and error, I got it working.
My code is posted below.
Remember to set your URLs in OAuth settings
These are mine using MAMP:
http://localhost:8888
http://localhost:8888/printing-forms/custom-stationery
TEAM DRIVE ID is found in Team Drive URL
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8"/>
<title>Google File Upload for Custom Stationary Orders</title>
<script type="text/javascript">
// The Browser API key obtained from the Google API Console.
// Replace with your own Browser API key, or your own key.
var developerKey = '[your API key]';
// The Client ID obtained from the Google API Console. Replace with your own Client ID.
var clientId = "[your Client ID]"
// Replace with your own project number from console.developers.google.com.
// See "Project number" under "IAM & Admin" > "Settings"
var appId = "[your project number]";
// Scope to use to access user's Drive items.
var scope = ['https://www.googleapis.com/auth/drive.file'];
var pickerApiLoaded = false;
var oauthToken;
// Use the Google API Loader script to load the google.picker script.
function loadPicker() {
gapi.load('auth', {'callback': onAuthApiLoad});
gapi.load('picker', {'callback': onPickerApiLoad});
}
function onAuthApiLoad() {
window.gapi.auth.authorize(
{
'client_id': clientId,
'scope': scope,
'immediate': false
},
handleAuthResult);
}
function onPickerApiLoad() {
pickerApiLoaded = true;
createPicker();
}
function handleAuthResult(authResult) {
if (authResult && !authResult.error) {
oauthToken = authResult.access_token;
createPicker();
}
}
// Create and render a Picker object for searching images.
function createPicker() {
if (pickerApiLoaded && oauthToken) {
// var view = new google.picker.View(google.picker.ViewId.DOCS);
// view.setMimeTypes("pdf");
var picker = new google.picker.PickerBuilder()
.enableFeature(google.picker.Feature.SUPPORT_TEAM_DRIVES)
// Hide list of files on the Drive
.enableFeature(google.picker.Feature.MINE_ONLY)
// Hide the Upload option for Personal Drive
// .enableFeature (google.picker.Feature.NAV_HIDDEN)
.addView(new google.picker.DocsUploadView()
// Custom Stationary Folder
.setParent('[Team Drive ID from URL]'))
.addView(new google.picker.DocsView(google.picker.ViewId.DOCS)
.setEnableTeamDrives(true)
.setSelectFolderEnabled(false)
// Upload files to Custom Stationary Folder
.setParent('[Team Drive ID from URL]'))
// Select more than one file
// .enableFeature(google.picker.Feature.MULTISELECT_ENABLED)
.setAppId(appId)
.setOAuthToken(oauthToken)
// Displays Personal Drive folder
// .addView(view)
.setDeveloperKey(developerKey)
.setCallback(pickerCallback)
.build();
picker.setVisible(true);
}
}
// A simple callback implementation.
function pickerCallback(data) {
if (data.action == google.picker.Action.PICKED) {
var fileId = data.docs[0].name;
// Show the ID of the Google Drive folder
document.getElementById('result').innerHTML = fileId + ' has been successfully uploaded';
// location.reload();
// alert('The user selected: ' + fileId);
}
}
</script>
</head>
<body>
<div id="result"></div>
<div id="continue">Back to the Order Form</div>
<!-- The Google API Loader script. -->
<script type="text/javascript" src="https://apis.google.com/js/api.js?onload=loadPicker"></script>
</body>
</html>

Direct to a new page after form submission

Currently I've developed a Google Scripts API which is used for people to upload files to a shared Google Drive folder. After the file are uploaded successfully, I want them to be taken to a separate "Thank you" page so it is clear their upload has worked. Currently I only have a message on the same page to this effect and I cannot figure out how to direct to a new page that I have created.
This is the additional bit I found from different questions to try direct to a new page however this is not working so far, as it remains on the same upload form page. I have included it at the bottom of my code.gs file. Any ideas on how to direct to a custom page that just says "thank you" or something similar would be great!
function doPost(e) {
var template = HtmlService.createTemplateFromFile('Thanks.html');
return template.evaluate();
}
The rest of my code is as follows:
Code.gs:
function doGet() {
return HtmlService.createHtmlOutputFromFile('form').setSandboxMode(
HtmlService.SandboxMode.IFRAME);
}
function createFolder(parentFolderId, folderName) {
try {
var parentFolder = DriveApp.getFolderById(parentFolderId);
var folders = parentFolder.getFoldersByName(folderName);
var folder;
if (folders.hasNext()) {
folder = folders.next();
} else {
folder = parentFolder.createFolder(folderName);
}
return {
'folderId' : folder.getId()
}
} catch (e) {
return {
'error' : e.toString()
}
}
}
function uploadFile(base64Data, fileName, folderId) {
try {
var splitBase = base64Data.split(','), type = splitBase[0].split(';')[0]
.replace('data:', '');
var byteCharacters = Utilities.base64Decode(splitBase[1]);
var ss = Utilities.newBlob(byteCharacters, type);
ss.setName(fileName);
var folder = DriveApp.getFolderById(folderId);
var files = folder.getFilesByName(fileName);
var file;
while (files.hasNext()) {
// delete existing files with the same name.
file = files.next();
folder.removeFile(file);
}
file = folder.createFile(ss);
return {
'folderId' : folderId,
'fileName' : file.getName()
};
} catch (e) {
return {
'error' : e.toString()
};
}
}
function doPost(e) {
var template = HtmlService.createTemplateFromFile('Thanks.html');
return template.evaluate();
}
Form.html:
<!DOCTYPE html>
<html>
<head>
<base target="_top">
</head>
<body>
<div align="center">
<p><img src="https://drive.google.com/uc?export=download&id=0B1jx5BFambfiWDk1N1hoQnR5MGNELWRIM0YwZGVZNzRXcWZR"
height="140" width="400" ></p>
<div>
<form id="uploaderForm">
<label for="uploaderForm"> <b> Welcome to the Tesco's animal welfare and soy reporting system. </b> </label>
<BR>
<BR>
<div style="max-width:500px; word-wrap:break-word;">
Please add your contact information below and attach a copy of your company's animal welfare standard before clicking submit. Wait for the browser to confirm your submission and you may then close this page.
<BR>
<BR>
Thank you very much for your submission.
</div>
<BR>
<BR>
<div>
<input type="text" name="applicantName" id="applicantName"
placeholder="Your Name">
</div>
<BR>
<div>
<input type="text" name="applicantEmail" id="applicantEmail"
placeholder="Your Company">
</div>
<BR>
<BR>
<div>
<input type="file" name="filesToUpload" id="filesToUpload" multiple>
<input type="button" value="Submit" onclick="uploadFiles()">
</div>
</form>
<br>
<br>
<br>
<br>
<br>
<br>
<div id="output"></div>
<script>
var rootFolderId = '1-aYYuTczQzJpLQM3mEgOkWsibTak7KE_';
var numUploads = {};
numUploads.done = 0;
numUploads.total = 0;
// Upload the files into a folder in drive
// This is set to send them all to one folder (specificed in the .gs file)
function uploadFiles() {
var allFiles = document.getElementById('filesToUpload').files;
var applicantName = document.getElementById('applicantName').value;
if (!applicantName) {
window.alert('Missing applicant name!');
}
var applicantEmail = document.getElementById('applicantEmail').value;
if (!applicantEmail) {
window.alert('Missing applicant email!');
}
var folderName = applicantEmail;
if (allFiles.length == 0) {
window.alert('No file selected!');
} else {
numUploads.total = allFiles.length;
google.script.run.withSuccessHandler(function(r) {
// send files after the folder is created...
for (var i = 0; i < allFiles.length; i++) {
// Send each file at a time
uploadFile(allFiles[i], r.folderId);
}
}).createFolder(rootFolderId, folderName);
}
}
function uploadFile(file, folderId) {
var reader = new FileReader();
reader.onload = function(e) {
var content = reader.result;
document.getElementById('output').innerHTML = 'uploading '
+ file.name + '...';
//window.alert('uploading ' + file.name + '...');
google.script.run.withSuccessHandler(onFileUploaded)
.uploadFile(content, file.name, folderId);
}
reader.readAsDataURL(file);
}
function onFileUploaded(r) {
numUploads.done++;
document.getElementById('output').innerHTML = 'uploaded '
+ r.fileName + ' (' + numUploads.done + '/'
+ numUploads.total + ' files).';
if (numUploads.done == numUploads.total) {
document.getElementById('output').innerHTML = 'All of the '
+ numUploads.total + ' files are uploaded';
numUploads.done = 0;
}
}
</script>
<label for="uploaderForm">
Powered by 3Keel
</label>
</body>
</html>
Thanks.html:
<!DOCTYPE html>
<html>
<head>
<base target="_top">
</head>
<body>
Thank you for submitting!
</body>
</html>
EDIT:
I have changed this function as recommended:
if (numUploads.done == numUploads.total) {
window.location = 'Thanks.html';
numUploads.done = 0;
Now it is redirecting to another page but I am faced with this error:
That’s an error.
The requested URL was not found on this server. That’s all we know.
If you are looking for the solution of your issue yet, how about this answer?
You want to open Thanks.html when the process at Form.html is finished.
Form.html and Thanks.html are put in a project.
If my understanding is correct, how about this workaround? I have ever experienced with your situation. At that time, I could resolve this issue by this workaround.
Modification points:
It is not required to use doPost() to access to Thanks.html. I think that you can achieve what you want using doGet().
I think that #Scott Craig's answer can be also used for this situation. In my workaround, the URL of window.location = 'Thanks.html'; is modified.
Uses the URL of deployed Web Apps. In your script, when users access to your form, they access to the URL of the deployed Web Apps. In this workaround, it is used by adding a query parameter.
Modified scripts:
Form.html
For the script added in your question as "EDIT", please modify as follows.
From:
window.location = 'Thanks.html';
To:
window.location = 'https://script.google.com/macros/s/#####/exec?toThanks=true';
https://script.google.com/macros/s/#####/exec is the URL of the the deployed Web Apps. Please add a query parameter like toThanks=true. This is a sample query parameter.
Code.gs
Please modify doGet() as follows.
From:
function doGet() {
return HtmlService.createHtmlOutputFromFile('form')
.setSandboxMode(HtmlService.SandboxMode.IFRAME);
}
To:
function doGet(e) {
if (e.parameter.toThanks) {
return HtmlService.createHtmlOutputFromFile('Thanks')
.setSandboxMode(HtmlService.SandboxMode.IFRAME)
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
} else {
return HtmlService.createHtmlOutputFromFile('form')
.setSandboxMode(HtmlService.SandboxMode.IFRAME);
}
}
Note:
When the script of the deployed Web Apps was modified, please redeploy it as new version. By this, the latest script is reflected to Web Apps.
The flow of this modified script is as follows.
When users access to the Form.html, because the query parameter of toThanks=true is not used, Form.html is returned.
When onFileUploaded() is run and if (numUploads.done == numUploads.total) {} is true, it opens the Web Apps URL with the query parameter of toThanks=true. By this, if (e.parameter.toThanks) {} of doGet() is true, and Thanks.html is returned and opened it.
In my environment, I could confirm that this modified script worked. But if this didn't work in your environment, I'm sorry. At that time, I would like to think of about the issue.
I might be misunderstanding your question, but from what I understand, instead of this line:
document.getElementById('output').innerHTML = 'All of the '
+ numUploads.total + ' files are uploaded';
You want to redirect to Thanks.html. If that's correct, just replace the above line with:
window.location = 'Thanks.html';

Google apps script strips characters away from postdata

When posting data to my google spreadsheet web app script, Google strips away the norwegian characters from the postdata. e.postData.contents is correct, but e.parameters is incorrect. See the code example below. When you press send, you will see that e.parameters and e.postData.contents returned by the google script are different.
To reproduce the problem with your own google spreadsheet web app:
Make a copy of my google spreadsheet
In the spreadsheet, select Tools->script editor
In the script editor, select Publish->Deploy as web app
In the dialog that opens, set "Who has access to the app" to "Anyone, even anonymous". Finally, press "Deploy".
You are now told that you need to give permissions to the script. Press "Review permissions". A message tells you that you should only allow this script to run if you trust the developer. Click on "Advanced" and click on the link at the bootom. Click allow.
In the new dialog that appears, copy the address of the "Current web app url".
In the code sample, replace the variable actionscript with the address you copied in step 6.
var actionScript = "https://script.google.com/macros/s/AKfycbxW1qHugD1K4adTjGAEt1KqbcbAn1LlaCoWx6GtlNdsNO_E-rTO/exec";
$(document).ready(function(){
var sendButton = $("#send");
if(sendButton != null)
{
sendButton.click(handleSend);
}
});
function handleSend(event) {
var data = $("#name").val();
console.log(data);
var postData = "name="+data;
console.log(postData);
request = $.ajax({
url: actionScript,
type: "post",
data: postData,
beforeSend: function () {
console.log("Loading");
},
error: function (jqXHR, textStatus, errorThrown) {
console.log(jqXHR);
console.log(textStatus);
console.log(errorThrown);
},
success: function (result) {
console.log("success");
var s = "<p>e.parameters=" + result.data + "</p><p>e.postData.contents="+result.postdata+"</p>"
$("#result").html(s);
debugger;
},
complete: function () {
console.log('Finished all tasks');
}
});
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<!doctype html>
<html lang="no">
<head>
<meta charset="utf-8">
</head>
<body>
<input id="name" type="text" value="GÅGHØHRÆR" size="50"/>
<button id="send">Send</button>
<div id="result">
</div>
</body>
</html>
My final solution so far :) is to just use encodeURIComponent to encode the data data posted to the google script:
encodeURIComponent( data )
On the google script side, e.parameters will decode this correctly, at least for the norwegian characters æøå.
The code snippet at the bottom is updated with encodeURIcomponent, i.e. using the solution described above.
----
Before I came to the conclusion above, I went through the following, which I thought could be of use to others as well:
window.btoa(data) did not encode the norwegian characters "æøå" in a the correct way. I had to do
window.btoa(unescape(encodeURIComponent( data )))
as suggested here: https://www.sumitgupta.net/javascript-base64-encode-decode-for-utf-8unicode-string/
https://www.base64decode.org/ and https://www.base64encode.org/ helped me figure this out as window.btoa("æøå")="5vjl" whereas entering æøå in https://www.base64encode.org/ with UTF-8 encoding gives "w6bDuMOl".
On the google app script side, I had to have this code:
var decodedData = Utilities.base64Decode(e.parameter["name"]);
var decodedDataAsString = Utilities.newBlob(decodedData).getDataAsString();
var actionScript = "https://script.google.com/macros/s/AKfycbwbYukbEejyL4yNlbW7xdfXPVZkZFJ7StxUIrKC/exec";
$(document).ready(function(){
var sendButton = $("#send");
if(sendButton != null)
{
sendButton.click(handleSend);
}
});
function handleSend(event) {
var data = $("#name").val();
var data2 = encodeURIComponent( data );
console.log(data2);
var postData = "name="+data2;
console.log(postData);
request = $.ajax({
url: actionScript,
type: "post",
data: postData,
beforeSend: function () {
console.log("Loading");
},
error: function (jqXHR, textStatus, errorThrown) {
console.log(jqXHR);
console.log(textStatus);
console.log(errorThrown);
},
success: function (result) {
console.log("success");
var s = "<p>e.parameters['name']=" + result.data + "</p><p>e.postData.contents="+result.postdata+"</p>"
$("#result").html(s);
},
complete: function () {
console.log('Finished all tasks');
}
});
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<!doctype html>
<html lang="no">
<head>
<meta charset="utf-8">
</head>
<body>
<input id="name" type="text" value="GÅGHØHRÆR" size="50"/>
<button id="send">Send</button>
<div id="result">
</div>
</body>
</html>

Creating a table using google app script from a query

I have successfully been able to create a google chart by querying a google sheet that I have. I'm trying to create a table by querying the same google sheet, but I'm not getting any results. I know I'm doing something wrong, but I'm not sure what it is :/ Here's the code below:
<html>
<head>
<script type="text/javascript" src="https://www.google.com/jsapi"></script>
<script type="text/javascript">
google.load("visualization", '1', {packages:['table']});
google.setOnLoadCallback(drawTable);
function drawTable() {
var query = new google.visualization.Query(
'https://docs.google.com/spreadsheet/ccc?key=1ZxBkX5lTidV2LJvSJYjBQfILF3PlIYjeNgDqsHZoMqo&tq=select%20D%2C%20S%2C%20T%2C%20U%20where%20D%3C%3E%22Avatar%22');
query.send(handleQueryResponse);
}
function handleQueryResponse(response) {
if (response.isError()) {
alert('Error in query: ' + response.getMessage() + ' ' + response.getDetailedMessage());
return;
}
var data = response.getDataTable();
var table = new google.visualization.Table(document.getElementById('table_div'));
var options = {'title':'Bass Naming',
'width':'927',
'height':'510'};
table.draw(data, options);
}
</script>
<title>Data from a Spreadsheet</title>
</head>
<body>
<span id='columnchart'></span>
</body>
</html>
var table = new
google.visualization.Table(document.getElementById('table_div'));
You do not have any element with id="table_div" in your html code, so you won't see you data table displayed in your page.
Check your browser's javascript console for any other errors you may have in your page.
Also, is this code from a general basic html application, or from an HTML file in Google Apps Script project, server by HTMLService? Just asking because GAS HTML Service does not always play well with Google Visualization API due to sandbox restrictions and Caja sanitization.