Google Apps script: Spreadsheet to Chart to Image - google-apps-script

I've created a function to take spreadsheet data, bring it to GAS, and then send it out to a webhook so I can use Zapier to build an image/pdf/etc. I used others samples and help on Google to figure out how to get the range, bring it over, etc and that's here:
const range = "Prior!H20:K23";
const [header, ...values] = SpreadsheetApp.getActiveSheet()
.getRange(range)
.getDisplayValues();
const table = Charts.newDataTable();
header.forEach((e) => table.addColumn(Charts.ColumnType.STRING, e));
values.forEach((e) => table.addRow(e));
const blob = Charts.newTableChart()
.setDataTable(table.build())
.setDimensions(500, 150)
.setOption("alternatingRowStyle", false)
.build()
.getBlob();
I then send the blob out, etc. The problem is that I'm losing all of the formatting from the spreadsheet, and since what I really want to do is almost just take a screenshot of the range in question, it's a problem. Specifically I'm losing text attributes, coloring, and the merging of cells.
Any ideas of how to do this, or, where in here I could modify/bring over the formatting?

There is no quick way to get a spreadsheet range with formatting through the SpreadsheetApp API.
See Tanaike's RichTextApp for one pretty simple method of getting formatted text content in a range of cells. The library that uses a temporary document to get the formatting.
To get a chart, complete with formatting, try .build().getAs('image/png') or 'image/svg' instead of .build().getBlob(). See Charts reference.
Alternatively, use Sheet.newChart() to get an EmbeddedTableChartBuilder instead of a TableChartBuilder.

You will need to extract formatting info from the sheet and feed it into the table builder
(Disclaimer, even by doing this you won't be able to do it with Apps Script, see below)
Especially if you use
.getDisplayValues();
Which will only get the values displayed from the sheet, no formatting information is taken from the sheet with this method.
You can use:
.getRichTextValues()
Though unfortunately a RichTextValue, is not recognized by the chart builder.
So the best thing is to break it up into a few steps, the first of which is using:
value.getTextStyle()
Which will then return a TextStyle object that has a few methods:
getFontFamily()
getFontSize()
getForegroundColor()
getForegroundColorObject()
isBold()
isItalic()
isStrikethrough()
isUnderline()
So now you can store the various formats you need to apply to your table.
However, the Apps Script Charts Service does not support this styling!
It seems that the Apps Script Chart Service does not support this type of styling. So maybe your only option is to open an HTML UI element via the UI service, or a Web app and then use the Google Charts service in the way demonstrated by the quickstart https://developers.google.com/chart/interactive/docs/quick_start.
You would need the setCell method.
This involves translating your styling to CSS, for example:
dataTable.setCell(22, 2, 15, 'Fifteen', {style: 'font-style:bold; font-size:22px;'});
// https://developers.google.com/chart/interactive/docs/reference#DataTable_setCell
File a feature request
You can always file a feature request for that by filling out this template:
https://issuetracker.google.com/issues/new?component=191640&template=823905
In the meantime though, you are probably best to get to grips with the JS library as it will always tend to be more complete than the Apps Script service that wraps it.
That, or build an HTML table without using Google Charts, then you could potentially use the HtmlService to pass it around in that format.
References
RichTextValue
DataTableBuilder
TableChartBuilder
UI service
Web apps
Charts
HtmlService

Related

How can I make an Import function run without opening the sheet?

I have a Google Sheet that uses an IMPORTRANGE query to combine data from multiple other sheets. This combined import sheet is read by Google AppSheet. We have realized that the data AppSheet is reading is always outdated. It only reads the data as of the last time the sheet was manually opened.
I followed the steps in this post to try to fix this issue by creating this function: function refresh() {SpreadsheetApp.flush()}. I then set up a timed trigger to activate it once an hour. Logs show the function is running, but the data is still not updating until I manually open the sheet.
This is my first time using Apps Script. Any tips/ideas? Is there a different or better way to have the formulas update without opening the file?
Thank you for reading.
SpreadsheetApp.flush() only works for the script execution that calls it. If you need to refresh the data results from a formula it's uncertain how exactly the spreadsheet will respond as most of the formula calculations are done on the client side. You could verify this by yourself by using your web browser developer tools.
Anyway, spreadsheet formulas have several caveats so it will not be extrange that at some point you will have to rethink your solution. Assuming that you want to keep using AppSheet:
Use AppSheet for your front end and some no-code / low-code automation. Keep your app small, if you need many forms / views consider to distribute them among several apps.
Use Google Sheets only for data storage for your AppSheet app. Please bear in mind that it has 10 million cells limit for the whole spreadsheet, so you might want to delete the unused sheets and delete the unused columns and rows on each sheet.
You might use Google Apps Script to do the data import and transformation tasks. If you need that something be updated based on actions done on the AppSheet app, you might use an installable change trigger or use webhook from the AppSheet side to and a "simple" web application using Google Apps Script (you could use GET / POST http requests to trigger some Google Apps Script functions).
Also you might use other programming platforms for the data import / transformation tasks and keep using Google Sheets as your AppSheet database by using the Google Sheets API or other automation tools like Zappier, IFTTT, Integromat among many others.
solution #1
You can try this solution :
define a checkbox (for instance in A1 in tab Sheet1)
set this script
function myFunction() {
var chk = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Sheet1').getRange('A1')
chk.setValue(false);
SpreadsheetApp.flush();
Utilities.sleep(500);
chk.setValue(true);
}
define a trigger on it
define the formula as follows
=if(A1,importrange("1n-rjSYb63Z2jySS3-M0BQ78vu8DTPOjG-SZM4i8IxXI","A:Z"),"")
when A1 is unchecked, the result will be empty, then check A1 to fill once again the result as expected
solution #2
by script, try for instance
function myFunction() {
var sh = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Sheet9')
var data = SpreadsheetApp.openById('1n-rjSYb63Z2jySS3-M0BQ78vu8DTPOjG-SZM4i8IxXI').getSheets()[0].getDataRange().getValues()
sh.getRange(1,1,data.length,data[0].length).setValues(data)
}
put a daily triger as needed

Google Apps Script - Creating and Saving Filters for Google Sheets

I feel like a bit of a chump, but I cannot work this out...
I have been given the job of producing a new master analysis sheet each month from a supplied XML file that combines with various columns of our (multiple) sheets. No problems, so far. I have got all of that working the way I want. :-)
My issue is that we also have about 6-8 filters saved with a specific sheet that allow our auditors to focus on specific areas (and as you can understand, our auditors want these to work EXACTLY as they specify).
I have tried using createFilter() but there doesn't appear any way to save multiple filters to that sheet (maybe I am missing something). No joy! :-(
I have tried recording a macro which I could then run to create the filters. No joy here either :-(
Do I have to tell these pesky auditors to create there own filters each month (they do know how, but it's beneath them), or is there a way I can script them up and get them off my back?
Unfortunately (as much as I would like to) I cannot share our sheets or scripts as we have significant IP embedded there.
I would really appreciate some guidance as to how you might approach this (if it is possible).
Kind regards
Ian
If you're indeed talking about the 'Create new filter view', I suggest making an template sheet. So instead of creating a new sheet every month, make one template spreadsheet and add all the filter views your auditors desire. Then copy that spreadsheet, and paste the new data in it.
The correct way to create a filter using Apps Script and the createFilter() is this one:
function setFilters() {
var ss = SpreadsheetApp.getActiveSheet();
var rangeFilter = ss.getRange("INPUT_YOUR_RANGE_HERE");
var filter = rangeFilter1.createFilter();
var filterCriteria = SpreadsheetApp.newFilterCriteria();
filterCriteria.ADD_YOUR_CRITERIA_HERE;
filter.setColumnFilterCriteria(columnPosition, filterCriteria.build());
}
As you can see, you must use build() in order to build the criteria for the filter you have created.
You can also use the Sheets advanced services and create the filters using the Sheets API, something similar to this:
var filterSettings = {
//YOUR FILTER SETTINGS
};
var request = [{
"setBasicFilter": {
"filter": filterSettings
}
}];
And as for calling the Sheets service and applying the above filter, you can use this:
Sheets.Spreadsheets.batchUpdate({'requests': request}, SPREADSHEET_ID);
Reference
Range Class Apps Script createFilter();
Filter Class Apps Script;
Apps Script Google Advanced Services.

Create static overview sheet based on dynamic and changing source sheet

Short description of what I am looking for:
A way to automatically import data from a dynamic Google Sheets document (using the Google Analytics add on) in another static Google Sheet, where the data will be automatically filled out in the right spot, but also stay there whenever the dynamic sheet changes.
A bit of context:
I am using Google's Analytics add on in Google Sheets (DOC1) to run reports about e.g. the number of users visiting my website every day. Due to the high amount of data, I can only run reports for about a month without sampling taking place.
For 2020 I would like to create an overview sheet (DOC2) to share with other people which gives an overview of the whole year. In other words, the cells of January in DOC2 should be filled when the data for January are rendered in DOC1. However, when data from February are being rendered in DOC1, the data from January should stay in DOC2.
I know it would be possible by copy and pasting the run reports to a non-dynamic sheet, but as I will need this for multiple parameters, this is not really a workable solution.
What I've tried
Multiple combinations of lookup and importrange functions, but the true difficulty stays in keeping the data once they are removed in the dynamic document. I have not found a solution to this problem yet...
If you are familiar with Apps Script, your request can be achieved with the method setValues()
Sample:
function myFunction(){
var origin=SpreadsheetApp.openById("ID of origin Spreadsheet").getSheetByName("Name of the Sheet");
var destination=SpreadsheetApp.openById("ID of destination Spreadsheet").getSheetByName("Name of the Sheet");
var range=origin.getDataRange();
var values=range.getValues();
var A1notation=range.getA1Notation();
destination.getRange(A1notation).setValues(values);
}
Fill into the code the IDs of your origin and destination spreadsheets, as well as the correct names of the origin and destination sheets.
Links to other useful methods:
SpreadsheetApp
getDataRange()
getA1Notation
getValues

Google apps script slow parsing

I was trying to parse vmstat using Google Apps script so that everyone in the company could use this to create a graph of the data. You can find my code here but this code is really slow. Is there something I can do to make this better or isn't Google Apps Script suitable for this? The problem the ammount of rows that needs to be processed. Any suggestions are welcome.
function doGet(){
var file = DriveApp.getFileById(id)
var docContent = file.getAs('application/octet-stream').getDataAsString();
var data = Charts.newDataTable()
.addColumn(Charts.ColumnType.STRING, 'TIME')
.addColumn(Charts.ColumnType.NUMBER, 'Memory');
var lines = docContent.split("\n");
Logger.log(lines.length);
var i = 1;
lines.forEach(function(line) {
if ((line.indexOf('mem') < 0) && (line.indexOf('free') < 0)) {
var values = line.match(/\S+/g);
data.addRow(['5',parseInt(values[3])]);
Logger.log(i)
}
if (i == 20){
return;
}
i++;
});
for( var i=0;i< lines.length;i++){
data.addRow(['5',10]);
}
data.build();
var chart = Charts.newAreaChart()
.setDataTable(data)
.setStacked()
.setRange(0, 400)
.setTitle('Memory')
.build();
return UiApp.createApplication().add(chart);
}
This isn't a problem of code optimization (although the code isn't perfect), as much as division of work.
The accepted approach to web application performance optimization involves separating three concerns; presentation, business logic and data accessref. With the exception of the generation of the vmstat output, you've got all of that in one place, making the user wait while you locate a file on Google Drive (using two exhaustive searches, btw) and then parse it into a Charts DataTable, and finally generate HTML (via UiApp).
You may find that the accessibility of a Google Apps Script presentation is useful to your organization. (I know in my workplace that our IT folks clamp down on in-house web servers, for example.) If so, consider what you have as prototype, and refactor it to give better perceived performance.
Presentation: Move from UiApp + Charts to HtmlService + Google Visualization. This moves the generation of the chart into the web client, instead of keeping it in the server. This will give a faster page load, to start.
Business Logic: This will be the rules that map your data into the Visualization. Like the Charts Service that is built over it, GViz uses DataTables with column definitions and rows of data.
One option here is to repeat the column definition & data load you already have, except on the client in JavaScript. Doing that will be significantly faster than via Google Apps Script.
A second option, which is even faster, especially with large datasets, is to load the data from an array.
google.visualization.arrayToDataTable(...)
Either way, you need to get your data to the JavaScript function that will build your chart.
Data Access: (I assume) you're currently running a shell script in Linux that calls vmstat and pipes the output to a file in your local Google Drive folder. (Alternatively, the script may be using the Drive API to push the file to Google Drive.) This file is plain text.
The change I'd make here would be to produce csv output from vmstat, and use Google Apps Script to import the csv into a spreadsheet. Then, you can use Sheet.getSheetValues() to read all the data in one shot, in a server side function to be called from the client JavaScript.
This would not be as fast as a local server solution, but it's probably the best way to do this using the Google Apps Script environment.
Edit: See more about this in my blog post, Converting from UiApp + Chart Service to Html Service + Google Visualization API.

Creating a Mobile User Interface for Google Spreadsheet

Is it possible to make an HTML interface for a spreadsheet that doesn't run inside the spreadsheet? Basically I want to use the spreadsheet as a simple database.
I can't seem to find a way to do it in the documentation. I got this to work this way:
var ss = SpreadsheetApp.getActive();
function onOpen() {
var html = HtmlService.createHtmlOutputFromFile('index');
ss.show(html);
That opens my page automatically when I load the sheet, which is not a bad way to have it work, but I would rather run it from a separate page without having to know it is looking at a spreadsheet.
Also, this script doesn't work on mobile browsers which is an issue.
Is what I want to do possible currently? I have been looking at the documentation for a while without a clear answer.
I believe you will be wanting to deploy your script as a web app, rather than a "container-bound" script inside a spreadsheet.
As there will be no spreadsheet inherently associated with the web app, you would need to use the openById() method rather than getActive().