Is it possible to write your own custom function in google sheets script that returns a drawn image, similar to how the SPARKLINE function works, except I want to make one that draws a pie chart instead.
I do not want to use Insert > Chart... > Pie Chart because that creates a floating chart on top of the spreadsheet. I would like to be able to write my own function that would return a pie chart that is embedded within the cell that the function is entered in, just like you can do with columns, bars, and line charts using sparkline.
How about following idea? This sample script embeds a chart to a cell using custom function on Spreadsheet. I think that this method is one of various ideas.
Problems :
When you want to create a chart and embed it to a cell using custom functions, you notice that insertChart() cannot be used. There are some limitations for using custom functions. But insertChart() creates floating charts. So in order to embed a chart to a cell, the function =IMAGE() is suitable for this situation. Here, setFormula() for setting =IMAGE() and DriveApp.createFile() for creating images from charts also cannot be used for custom functions.
Solution :
In order to avoid these limitations, I used Web Apps.
To use this sample script, please deploy Web Apps as follows.
On the Script Editor,
File
-> Manage Versions
-> Save New Version
Publish
-> Deploy as Web App
-> At Execute the app as, select "your account"
-> At Who has access to the app, select "Anyone, even anonymous"
-> Click "Deploy"
-> Copy "Current web app URL"
-> Click "OK"
When it deploys Web Apps, the approval required authorization can be done, simultaneously.
Sample Script :
Please copy and paste this script to a bound script of spreadsheet.
var folderId = "### Folder ID ###"; // This is a folder to save images.
var webappsurl = "https://script.google.com/macros/s/######/exec"; // Here, please put "Current web app URL".
function embedChart(range) {
var ac = SpreadsheetApp.getActiveSheet().getActiveCell();
var q1 = "?datarange=" + range;
var q2 = "&row=" + ac.getRow();
var q3 = "&col=" + ac.getColumn();
var url = webappsurl + q1 + q2 + q3;
UrlFetchApp.fetch(url);
}
function doGet(e) {
var sheet = SpreadsheetApp.getActiveSheet();
var chart = sheet.newChart()
.setChartType(Charts.ChartType.PIE)
.addRange(sheet.getRange(e.parameters.datarange))
.setOption('height', 280)
.setOption('width', 480)
.setOption('title', 'Sample chart')
.build();
var file = DriveApp.getFolderById(folderId).createFile(
chart.getAs('image/png').setName("chart_image.png")
);
file.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW);
sheet.getRange(e.parameters.row, e.parameters.col).setFormula(
'=IMAGE("' + "http://drive.google.com/uc?id=" + file.getId() + '")'
);
}
Flow of Script :
embedChart()
Input =embedChart("a2:a6") in cell B7.
Using fetch(), sends data of a2:a6 and the inputted coordinate to doGet().
doGet()
Using doGet(), get the data.
Creates a chart using inputted range a2:a6. (in this case, creates a pie chart)
Saves a chart as an image. (in this case, saves as PNG)
Updates a permission of the image file to use for =IMAGE().
Embeds the image using =IMAGE() which was imported by setFormula().
Result :
By inputting =embedChart("a2:a6") in cell B7 as a custom function, following result can be obtained.
Note :
When the custom function embedChart() is used, loading time is about 40 seconds. (I don't know whether this occurs at only my environment.)
Permissions of the created image are ANYONE_WITH_LINK, VIEW.
embedChart() is overwritten by =IMAGE(). So when the spreadsheet is reopened, the response of =IMAGE() is much faster than that of embedChart().
If I misunderstand your question, I'm sorry.
Related
I created a custom function for google spreadsheets. All it does is return a random letter. The function works great when I first enter it into a cell. But now I want to be able to "recalculate" the function using a keyboard shortcut; I'd also be willing to refresh the page if needed.
TLDR: I want to be able to hit a key and have my custom functions recalculate.
How can I accomplish this?
Edit to add:
Here is the code for my function.
//returns a random letter suitable for use in function notation
function ranFunLet() {
var letters = ['a','b','c','d','f','g','h','j','k','m','n','p','q','r','s','t','u','v','w','x','y','z']
var letter = letters[Math.floor(Math.random()*letters.length)];
//console.log(letter);
return letter;
}
I would like the cell I use it in to run the function again when I press a button (or refresh the page).
Suggestion
The Apps Script editor does support keyboard shortcut trigger as per this existing answer. However, you may want to try Importing functions as macros, then you can assign a unique keyboard shortcut to it.
Here's a sample
Sample Sheet
Sample script function to test
This sample script function increments the number on A1 cell.
function sample() {
var data = SpreadsheetApp.getActive().getActiveSheet().getRange("A1").getValue();
var res = data+1;
SpreadsheetApp.getActive().getActiveSheet().getRange("A1").setValue(res);
}
Import the function on your spreadsheet (in my testing it is named as sample):
In the Google Sheets UI, select Tools > Macros > Import.
Select a function form the list presented and then click Add
function.
Select clear to close the dialog.
Select Tools > Macros > Manage macros.
Locate the function you just imported in the list. Assign a unique
keyboard shortcut to the macro. You can also change the macro name
here; the name defaults to the name of the function.
Click Update to save the macro configuration.
Result
After pressing the sample shortcut key Ctrl + Alt + Shift + 2, the function incremented the number on A1 cell from 1 to 2:
NOTE: You can not choose a specific shortcut & if you'll edit your function on the Apps Script editor, you would need to re-import your function again as a macro.
the button solution is done like this:
https://www.youtube.com/watch?v=yaBMsSpAxYM
I have a google sheet - and when a new row appears I am writing the output into a Google Document using a predefined template via a merge.
All is working but as I could only work out how to use the .replaceText() function to achieve the merge, the hyperlinks in some of the sheet columns get exported as plain text.
After much fiddling and cribbing of code (thanks all) I managed to cobble together the following function:
function makeLinksClickable(document) {
const URL_PATTERN = "https://[-A-Za-z0-9+&##/%?=~_|!:,.;]+[-A-Za-z0-9+&##/%=~_|]"
const URL_PATTERN_LENGTH_CORECTION = "".length
const body = document.getBody()
var foundElement = body.findText(URL_PATTERN);
while (foundElement != null) {
var foundText = foundElement.getElement().asText();
const start = foundElement.getStartOffset();
const end = foundElement.getEndOffsetInclusive() - URL_PATTERN_LENGTH_CORECTION;
const url = foundText.getText().substring(start,end+1)
foundText.setLinkUrl(url)
foundElement = body.findText(URL_PATTERN, foundElement);
}
}
After writing out all the columns to the document I call this function on the created document to look for a hyperlink and make it hyper :)
As long as each cell only contains one hyperlink my function works.
It also works where there are multiple hyperlinks in the document.
However, some cells can have multiple hyperlinks and writes them out to the document with a new line for each one.
Although the function finds the multiple URLs correctly and makes them clickable in the document there is a problem.
For example, if there are 2 hyperlinks in the cell they get exported to 2 lines in the document, but after running them through the function - both hyperlinks will now link to the same image (the first) even though each hyperlink itself is the unique link from the original cell.
2 converted hyperlinks that link to the same image
(Note - If I don't run my function and leave the exported hyperlinks as text. Then go into the created document and manually add a space to the ends of the exported hyperlinks then they turn blue and become clickable and link to the correct image, I did try to add a space programmatically before this but couldn't work that out either)
I have exhausted my limited coding ability and can't see why my function which "seems" to work its way through each hyperlink correctly doesn't make it then link to the right image in the document.
Any help would be most appreciated.
Thanks
// ----------------------------------------------------------------------
Thank you for taking the time to look at this, I will try to explain the issues further. It is hard to show here as the links actually work properly when copied here they only misbehave in the google document.
A cell in the exported row has multiple hyperlinks separated by a comma.
they get exported from the cell to the document as text strings like this:
Links in single Sheets Cell for exporting:
"hyperlink-1-as-a-string", (links to image 1)
"hyperlink-2-as-a-string", (links to image 2)
"hyperlink-3-as-a-string", (links to image 3)
"hyperlink-4-as-a-string", (links to image 4)
"hyperlink-5-as-a-string" (links to image 5)
I then run my funtion to make them clickable again.
If there are two are more hyperlinks in the same cell when exported then I get the following issue after running the function.
Exported Text links converted by to clickable hyperlinks:
"hyperlink-1-as-a-string", (links to image 5)
"hyperlink-2-as-a-string", (links to image 5)
"hyperlink-3-as-a-string", (links to image 5)
"hyperlink-4-as-a-string", (links to image 5)
"hyperlink-5-as-a-string" (links to image 5)
I "think" what happens is that my function makes all 5 hyperlinks one big hyperlink that happens to use the last hyperlinks image.
If I copy and paste the URLs into a separate document like an email then they appear as one large hyperlink, not 5 separate ones.
// ---------------------------------------------------------------
The function searches for text patterns that are in fact google hyperlinks.
(starting https:// etc)
When it finds one it works out the length to the end of the text string and then uses setLinkUrl() to make the hyperlink - into a clickable hyperlink.
If there is only one text hyperlink then it works.
If there is more than one text hyperlink, separated by commas then it does not.
I worked something out. This is what I ended up with, it is basically put together from a few other questions & answers - It's not very clever but it works.
Thanks to the various posters who enabled me to figure this out.
function sortLinks(colId, mapPoint, myBody) {
var urls = [];
if (colId.includes(",")) { // IE theres more than one URL
var tmp = colId.split(",");
urls = urls.concat(tmp);
}
else {
urls[0] = colId; // 1 URL no "," add to array[0]
}
if (urls.length > 0) {
var tag = mapPoint;
var newLine = "\n";
var element = myBody.findText(tag);
if (element) {
var start = element.getStartOffset();
var text = element.getElement().asText();
text.deleteText(start, start + tag.length - 1);
urls.forEach((url, index) => {
url = url.trim();
var name = "Image-Video" + (index + 1);
text.appendText(name).setLinkUrl(start, start + name.length - 1, URL);
text.appendText(newLine);
start = start + name.length + newLine.length;
});
}
my frient shared his google sheet to me and the table contains image which is a link (url). How can i make a copy of this sheet and make all the image link to be local, so i want the image is copying to my local google drive automatically (so the link won't be broken if he delete his images files in future). Right now, if i make a copy of this document, then it still link to original image source.
How is it possible ? of course i don't want to manually copy them one by one from the link. Is there any better and faster way ?
https://docs.google.com/spreadsheets/d/1TkXwAd8rKbjnGfYEJVaOYBJwCZ7G7YfuSvmcDE6g8No/edit?usp=sharing
The OP wants to extract the image URL from a hyperlink formula, and save a copy of the image to their own Google Drive account.
This answer combines several elements from precedents on StackOverflow.
Since the images metadata is in the formula, the code uses the getFormulas() method rather than the "conventional" getValues(). Cells with no formula are empty strings; hence the test if (formula.length !=0){.
Get the file name without extension: REGEX: Capture Filename from URL without file extension. Ironically, this precedent doesn't use regular expressions but finds the position of the last / and the last . using lastIndexOf and getting a substring between those points. Note this solution fails on filenames with multiple periods, though there is an alternative solution for this scenario.
Get the file name from the url: Getting a Google Spreadsheet Cell's Image URL which combines regex and Javascript match.
Save a file to Google Drive: Need sheets script to save img to drive which is a simple and elegant solution for saving files.
Saving the file to Google Drive: When copying files using Apps Script from one folder to another any “Apps Script” files being copied end up in MyDrive not the specified folder - why? explains why the API is required to write the files to My Drive.
Note: In order to use this script, enable Drive API v2 at Advanced Google Services
On script editor, Resources -> Advanced Google Services; Turn on Drive API v2
function so5811567402() {
var ss=SpreadsheetApp.getActiveSpreadsheet();
var sheetName = "Table";
var sh = ss.getSheetByName(sheetName);
var rg=sh.getDataRange();
var lastColumn = sh.getLastColumn();
var lastRow = sh.getLastRow();
var formulas = rg.getFormulas();
for (var i in formulas) {
for (var j in formulas[i]) {
var formula = formulas[i][j];
if (formula.length !=0){
var regex = /image\("(.*)"/i;
var matches = formula.match(regex);
var imgurl = matches[1];
var filename = imgurl.substring(imgurl.lastIndexOf("/") + 1, imgurl.lastIndexOf("."));
//Logger.log(filename);
var image = UrlFetchApp.fetch(imgurl).getBlob().getAs('image/jpeg').setName(filename);
var FolderId = "Folder ID goes here";
var folder = DriveApp.getFolderById(FolderId);
var file = DriveApp.createFile(image);
Drive.Files.update({"parents": [{"id": folder.getId()}]}, file.getId());
}
}
}
}
I have an existing google form and am looking to:
Image 1. of the google form question.
1) Have the response to the question (What is your name) in the form automatically populate (Sheet 1, Column C) on this existing google sheet
Image 2. Where the google form data will have to go
2) The timestamp that gets generated with each google form submission to automatically populate (Sheet 1, Column E) in the YYYY-MM-DD format.
3) While these google form responses will be recorded in this spreadsheet there will be times when I will have to manually go in and enter information in subsequent rows as well.
Is this possible to do? I am new to bringing in data from google forms into google sheets, can anyone help with the questions above?
Okay. A couple of things.
Go to the Tool menu > Script editor.
Name the script (maybe 'Form Submission'?) by clicking the 'untitled project' text in the top left of the editor.
Replace all text in code.gs with the code below. (Change the code where indicated).
Then go to Edit > Current project's triggers.
Click the link that says: No triggers set up. Click here to add one now.
Under Run, select onSubmit.
Under Events, select on form submit.
Click save.
Now you should go back to the editor and push the play button. This will run the function and initiate the authorisation process. Click through the prompts and accept.
Now, every time a form is submitted, the name and timestamp will be copied over.
function onSubmit() {
var spreadsheet = SpreadsheetApp.getActive();
var responseSheet = spreadsheet.getSheetByName('Form Responses 1');
var copyToSheet = spreadsheet.getSheetByName('Target');
var rLastRow = responseSheet.getLastRow();
var tLastRow = copyToSheet.getLastRow() + 1;
var lastCol = responseSheet.getLastColumn();
var values = responseSheet
.getRange(rLastRow, 1, 1, lastCol)
.getValues()[0];
var timestamp = Utilities.formatDate(new Date(values[0]), Session.getScriptTimeZone(), 'yyyy-MM-dd');
var name = values[1];
copyToSheet.getRange('C' + tLastRow).setValue(name);
copyToSheet.getRange('E' + tLastRow).setValue(timestamp).setNumberFormat('yyyy-MM-dd');
}
I have a very large sheet of data with comments applied to different projects (totally 16). Each comment has a status such as active, confirmed, cancelled or blank (if not applicable) applied to the projects
It's difficult for users to update the status with filters in the source sheet because when many users using the sheet they cannot apply different filters at the same time.
The best way is to pull all data from the source sheet to many target sheets (one for each project) and apply filters according to project, so the users can update the status in target sheets. My problem is how to get the new updated status from the target sheets into the source sheet.
I have found a script to automatically update all target sheets whenever I change or add something in the source sheet, but I need also to update the source sheet when I change the status in the target sheet (project sheets). See script below:
function getdata() {
var files = DriveApp.getFolderById(""folder key with target sheets"").getFiles()
while (files.hasNext()) {
var file = files.next();
var shoot = SpreadsheetApp.openById(file.getId());
var sourcesheet = SpreadsheetApp.getActive().getSheetByName(""source sheet name"");
var sourcerange = sourcesheet.getRange('A:AE');
var sourcevalues = sourcerange.getValues();
var destsheet = shoot.getSheetByName('target sheets name same for all');
var destrange = destsheet.getRange('A:AE');
destrange.setValues(sourcevalues);
See below a link of my source sheet:
https://docs.google.com/spreadsheets/d/1h0mpPo2nl9AoCF-hJDjMaU3sZg-qBX4dat7Ig4poAJo/edit?usp=sharing
The sheet "form responses new" receiving comments from users through a form and I manually cut and paste them in sheet ""Sent"" after review and submission.
You might want to look into making filter views for each project in the single sheet. Filter views are applied only for the person using it and do not modify it for others. From your question this may be a better solution than a syncing script.
However to answer the question asked an onEdit event in each of the project sheets could manage the reverse updates.
function onEdit(event){
var range = event.range;
if(range.getSheet().getName() == "Sheet1" && range.getColumn() > 1 && range.getRow() > 1 ){//example conditionals to limit the action to exclude headers or other static data could be changed to work specific cells or ranges
var mainsheet = SpreadsheetApp.openByUrl("main spreadsheet URL").getSheetByName("destination sheet");
var dataA1 = range.getA1Notation();
range.copyTo(mainsheet.getRange(dataA1));
}
}
this is a simple trigger and requires your users to have edit access to your main sheet. If you do not want to give them that access you could use an installable trigger instead.