I tried applying this solution to my case:
Emailing SPARKLINE charts sends blank cells instead of data
But when I try to apply it to my situation an error pops up with:
TypeError: Cannot read property '0' of null
On the executions there is more information about this error:
My GAS code for my Email solution is able to send just the values, and it's here:
function alertDailyInfo() {
let emailAddress = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("SANDBOX").getRange("F1").getValue();
let treeIconUrl = "https://d1nhio0ox7pgb.cloudfront.net/_img/g_collection_png/standard/256x256/tree.png";
let treeIconBlob = UrlFetchApp
.fetch(treeIconUrl)
.getBlob()
.setName("treeIconBlob");
let treeUpdate = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("SANDBOX").getRange("F6").getValue();
let waterUpdate = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("SANDBOX").getRange("F11").getValue();
if (treeUpdate > 0) {
MailApp.sendEmail({
to: emailAddress,
subject: "TREE WATER UPDATE",
htmlBody: "<img src='cid:treeIcon'><br>" + '<br>' + '<br>' +
'<b><u>Tree average is:</u></b>'+ '<br>' + treeUpdate + '<br>' + '<br>' +
'<b><u>Water average is:</u></b>'+ '<br>' + waterUpdate + '<br>' + '<br>'
,
inlineImages:
{
treeIcon: treeIconBlob,
}
});
}
}
The code from the solution presented on the link above and which I have tried to adapt to my situation (please check my file below) is here:
drawTable();
function drawTable() {
let emailAddress1 = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("SANDBOX").getRange("F1").getValue();
var ss_data = getData();
var data = ss_data[0];
var background = ss_data[1];
var fontColor = ss_data[2];
var fontStyles = ss_data[3];
var fontWeight = ss_data[4];
var fontSize = ss_data[5];
var html = "<table border='1'>";
var images = {}; // Added
for (var i = 0; i < data.length; i++) {
html += "<tr>"
for (var j = 0; j < data[i].length; j++) {
if (typeof data[i][j] == "object") { // Added
html += "<td style='height:20px;background:" + background[i][j] + ";color:" + fontColor[i][j] + ";font-style:" + fontStyles[i][j] + ";font-weight:" + fontWeight[i][j] + ";font-size:" + (fontSize[i][j] + 6) + "px;'><img src='cid:img" + i + "'></td>"; // Added
images["img" + i] = data[i][j]; // Added
} else {
html += "<td style='height:20px;background:" + background[i][j] + ";color:" + fontColor[i][j] + ";font-style:" + fontStyles[i][j] + ";font-weight:" + fontWeight[i][j] + ";font-size:" + (fontSize[i][j] + 6) + "px;'>" + data[i][j] + "</td>";
}
}
html += "</tr>";
}
html + "</table>"
MailApp.sendEmail({
to: emailAddress1,
subject: "Spreadsheet Data",
htmlBody: html,
inlineImages: images // Added
})
}
function getData(){
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("SANDBOX");
var ss = sheet.getDataRange();
var val = ss.getDisplayValues();
var background = ss.getBackgrounds();
var fontColor = ss.getFontColors();
var fontStyles = ss.getFontStyles();
var fontWeight = ss.getFontWeights();
var fontSize = ss.getFontSizes();
var formulas = ss.getFormulas(); // Added
val = val.map(function(e, i){return e.map(function(f, j){return f ? f : getSPARKLINE(sheet, formulas[i][j])})}); // Added
return [val,background,fontColor,fontStyles,fontWeight,fontSize];
}
// Added
function getSPARKLINE(sheet, formula) {
formula = formula.toUpperCase();
if (~formula.indexOf("SPARKLINE")) {
var chart = sheet.newChart()
.setChartType(Charts.ChartType.SPARKLINE)
.addRange(sheet.getRange(formula.match(/\w+:\w+/)[0]))
.setTransposeRowsAndColumns(true)
.setOption("showAxisLines", false)
.setOption("showValueLabels", false)
.setOption("width", 200)
.setOption("height", 100)
.setPosition(1, 1, 0, 0)
.build();
sheet.insertChart(chart);
var createdChart = sheet.getCharts()[0];
var blob = createdChart.getAs('image/png');
sheet.removeChart(createdChart);
return blob;
}
}
The code that is working just for the values, which I pasted above (1st block of code), will send me an email like this:
But I need to receive the email like this, with the Sparklines below the values like so:
The code for the Email solution, just for the values, I pasted above (1st block of code) is working. But for some reason when the code from the solution linked above (2nd block of code) is imported/saved into my Google Sheets file GAS script library and adapted to my case, everything stops working, displaying the errors mentioned above.
So basically, as you might have already understood, I need to send emails with the values from Tree Average and Water Average, and I managed to get that working. But I also need for the Sparkline graphs that you can see below, and by checking my file linked below too, to also be sent as images/blobs, just below the info, like in the screenshot above.
Can anyone provide any pointers on what can be missing in applying the solution above or is there a better alternative to sending a SPARKLINE graph as image/blob by email?
Here is my file:
https://docs.google.com/spreadsheets/d/1ExXtmQ8nyuV1o_UtabVJ-TifIbORItFMWjtN6ZlruWc/edit?usp=sharing
EDIT_1:
I made some edits to bring more clarity.
EDIT_2:
As requested this is the formula applied to the first Sparkline, the 2nd one is pretty much the same:
=ARRAYFORMULA( SPARKLINE(QUERY({IFERROR(DATEVALUE(SANDBOX!$A$2:$A)), SANDBOX!$B$2:$B},
"select Col2
where Col2 is not null
and Col1 <= "&INT(MAX(SANDBOX!$A$2:$A))&"
and Col1 > "&INT(MAX(SANDBOX!$A$2:$A))-(
IFERROR(
VLOOKUP(
SUBSTITUTE($F$4," ",""),
{"24HOURS",0;
"2DAYS",1;
"3DAYS",4;
"7DAYS",8;
"2WEEKS",16;
"1MONTH",30;
"3MONTHS",90;
"6MONTHS",180;
"1YEAR",365;
"2YEARS",730;
"3YEARS",1095},
2,FALSE))
)-1, 0),
{"charttype","column";"color","#00bb21";"empty","ignore";"nan","ignore"}))
EDIT_3: At the advice of Rubén I have removed drawTable(); at the beggining of the code block.
I have also transfered the formula for the Sparkline to another helper sheet and link it to the main sheet.
After trying it seems the error does not appear anymore. Although the email received has 2 problems:
I receive the whole sheet in table form, where I just wanted the Sparklines.
Also the Sparklines do not come as images, they do not show up at all. Also where they should appear it says undefined.
I guess the whole sheet is being set because the function getting the range getDataRange(); is getting the whole sheet range.
Here is a screenshot:
As the question you reference explains:
the chart created by SPARKLINE cannot be directly imported to the email.
Why isn't the script working? Because you have not made any significant modifications to it and because you are using a more complex formula than the one proposed in the other question, it is very difficult (if not impossible) to make it work without any modifications.
What are the options? In my opinion you have 3 different options.
Follow the logic of the solution proposed by Tanaike in the other question and using EmbeddedChartBuilder try to shred the content of the FORMULA to achieve the same as with SPARKLINE.
Use the SpreadsheetApp methods to directly get the values from the sheet and build the chart from there.Here is a small example of how you can do it using Chart Service (You could achieve exactly the same with EmbeddedChartBuilder). As you already have a Blob object, you can insert it inside an email as I do inside the Sheet.
function constCreateChart() {
const sS = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('HELPER')
const chart = Charts.newDataTable()
.addColumn(Charts.ColumnType.NUMBER, '')
.addColumn(Charts.ColumnType.NUMBER, '')
// Modfify with your data
// getRange('A2:A15').getValues()...
const builder = [...Array(100).keys()].forEach(n => {
chart.addRow([n, n * n * Math.random()])
})
chart.build()
const chartShap = Charts.newColumnChart()
.setDataTable(chart)
.setLegendPosition(Charts.Position.NONE)
.setOption('hAxis.ticks', [])
.setOption('vAxis.ticks', [])
.build()
sS.insertImage(chartShap.getAs('image/png'), 5, 5)
}
Result
Use this form to request Google to add the possibility to convert charts obtained using SPARKLINES to Blob objects that can be used inside an email.
Documentation
Avalible Options in Chart Service
Fundamentals of Apps Script with Google Sheets #5:Chart and Present Data in Slides
Remove drawTable(); as this line makes that the drawTable function be executed when any function be called.
Apparently the error occurs on .addRange(sheet.getRange(formula.match(/\w+:\w+/)[0])), more specifically because formula.match(/\w+:\w+/) (this expression is intended to extract a range reference of the form A1:B10) returns null. Unfortunately the question doesn't include the formula. One possible solution might be as simple as replacing sheet.getRange(formula.match(/\w+:\w+/)[0]) by another way to set the source range for the temporary chart, but might be a more complex, i.e. adding a helper sheet to be used as the data source for the temporary chart.
NOTE: On Rev 11 one in-cell sparklines chart formula was added. As the formula is pretty complex, the simplest solution is to add a helper sheet to add the QUERY function
QUERY({IFERROR(DATEVALUE(SANDBOX!$A$2:$A)), SANDBOX!$B$2:$B},
"select Col2
where Col2 is not null
and Col1 <= "&INT(MAX(SANDBOX!$A$2:$A))&"
and Col1 > "&INT(MAX(SANDBOX!$A$2:$A))-(
IFERROR(
VLOOKUP(
SUBSTITUTE($F$4," ",""),
{"24HOURS",0;
"2DAYS",1;
"3DAYS",4;
"7DAYS",8;
"2WEEKS",16;
"1MONTH",30;
"3MONTHS",90;
"6MONTHS",180;
"1YEAR",365;
"2YEARS",730;
"3YEARS",1095},
2,FALSE))
)-1, 0)
Then instead of sheet.getRange(formula.match(/\w+:\w+/)[0]) use helperSheet.getDataRange(). You will have to set an appropriate way to declare helperSheet.
Related to Rev. 8
The code on Tanaike's answer reads data from Sheet1 but your sheet is named SANDBOX.
Working with some legacy speghatti code with very limited knowledge.
There are two different charts jqPlot . First chart represnts Total number of countries in each YearMonth. Second chart would represent all the country name and there distinct number. For 2nd Chart the number of each country will always be 1, however the countries themselves will be one or more.
1st chart (as above) is generated correctly and selecting one BAR from the chart query returns correct name for the countries. But problem occurs while plotting the second chart. Gives error. What am I missing?
ERROR : Subscript out of range: '[number: 1]'
The following piece of code creates JSON through an AJAX call a nd sends to the Jquery function to create a Chart.
' Create categories
cat="["
for i = 0 to recCount
if i<>0 then cat=cat&"," end if
cat=cat&""""&data(0,i)&""""
next
cat = cat & "]"
DebugWrite("jsonCat:" & cat)
startYear= ymStart \ 100
startMonth=ymStart mod 100
periodMonths=25
json="[["
y=startYear
m=startMonth
for i=0 to recCount
DebugWrite("recCount:" & recCount)
if i<>0 then json=json&"," end if ' separator for all but first
json=json&"["&i+1
if data(1,i)<>0 then ' check if there are records to prevent / 0
json=json&","& data(1,i)&","""&data(1,i)&""","""&data(0,i)&"""]" '<<<<<THIS LINE CREATING PROBLEM>>>>>>>>>
else
json=json&",0,""n=0""]" ' put empty json data if no records
end if
next
json=json&"]]"
' clear data array, information now in JSON
set data = nothing
The query for the DATA is as follows:
detLevel="Country"
sqlWhere="1=1"
strSQL = "SELECT "&detLevel&" from"&_
" (SELECT "&detLevel&",COUNT(Distinct c.Country) as CountryCount"&_
" FROM BIDashboard.dbo.ISO c"&_
" WHERE ym is not null AND ym="&ym&_
" AND "&sqlWhere&" GROUP BY "&detLevel&") as x"&_
" WHERE "&detLevel&" IN (SELECT DISTINCT "&detLevel&" FROM dbo.ISO)"&_
" ORDER BY 1"
I was making a survey in Qualtrics, and needed to have my items show different values of the slider depending on a variable, in my case, the value from a loop and merge. That didn't seem like a thing that you could do with piped text, so I had to figure out how to do it in Javascript.
I'm just posting this as an opportunity to provide the answer I found on my own. As usual with Qualtrics, your mileage may vary, and this may need to be modified for your specific situation. In particular, the question IDs and postTags change depending on whether it is in a loop/merge, and perhaps on other factors.
Put the following code into the javascript section of the question:
// Set the slider range
// First define the function to do it
setSliderRange = function (theQuestionInfo, maxValue) {
var postTag = theQuestionInfo.postTag
var QID=theQuestionInfo.QuestionID
// QID should be like "QID421"
// but postTag might be something like "5_QID421" sometimes
// or, it might not exist, so play around a bit.
var sliderName='CS_' + postTag
window[sliderName].maxValue=maxValue
// now do the ticks. first get the number of ticks by counting the table that contains them
var numTicks = document.getElementsByClassName('LabelDescriptionsContainer')[0].colSpan
// do the ticks one at a time
for (var i=1; i<=numTicks; i++) {
var tickHeader='header~' + QID + '~G' + i
// the first item of the table contains the minimum value, and also the first tick.
// so we do some tricks to separate them out in that case.
var tickSpanArray = $(tickHeader).down("span.TickContainer").children
var tickSpanArrayLength=tickSpanArray.length
var lastTickIndex=tickSpanArrayLength - 1
var currentTickValue = tickSpanArray[lastTickIndex].innerHTML
currentTickValue=currentTickValue.replace(/^\s+|\s+$/g,'')
console.log('Tick value ' + i + ' is ' + currentTickValue)
// now get the new value for the tick
console.log('maxValue: ' + maxValue + ' numTicks: ' + numTicks + ' i: ' + i)
var newTickValue = maxValue * i / numTicks //the total number of ticks
tickSpanArray[lastTickIndex].innerHTML=newTickValue.toString()
console.log('Changed tick value to ' + newTickValue)
}
}
var currentQuestionInfo = this.getQuestionInfo()
var currentQuestionID = currentQuestionInfo.QuestionID
// Now call the function
setSliderRange(currentQuestionInfo, theMaxValueYouWant)
If you find my answers helpful, help raise my reputation enough to add "qualtrics" as a valid tag!! Or, if someone else with reputation over 1500 is willing to do it that would be very helpful!
I am using getRowsData to retrieve the information, but I would like to modify the values stored within a JS object and then write that modified value back into the cell. I would like to use the normalized header variable that was created with getRowsData to write back into the cell. So if I retrive the info using
var thirdEmployee = employeeObjects[2];
var stringToDisplay = "The third column is: " + thirdEmployee.firstName + " " + thirdEmployee.lastName;
stringToDisplay += " (id #" + thirdEmployee.employeeId + ") working in the ";
stringToDisplay += thirdEmployee.department + " department and with phone number ";
stringToDisplay += thirdEmployee.phoneNumber;
ss.msgBox(stringToDisplay);
Then, for example, I would assign a new value to the Javascript object thirdEmployee.phoneNumber; and then have it written into the proper location in the range (i.e., active row, column # based on header).
thirdEmployee.phoneNumber = "123-555-5555";
thirdEmployee.phoneNumber.setNewValue();
or
setNewValue.thirdEmployee.phoneNumber;
Basically, it would be a modified version of setRowsData but allow for modification of one object and then telling that object to be written into the spreadsheet based on the active row & column based on the header.
Anyone use or know how to do this?
The setRowsData() helper function from the Writing Data from JavaScript Objects to a Spreadsheet tutorial already supports the ability to write a single object. To use it, though, you need to provide both of the optional parameters optHeadersRange and optFirstDataRowIndex.
If we assume that your headers are in row 1, here's how you could update thirdEmployee:
var headersRange = sheet.getRange(1, 1, 1, sheet.getLastColumn());
var thirdEmployee = employeeObjects[2];
...
thirdEmployee.phoneNumber = "123-555-5555";
setRowsData(sheet, [thirdEmployee], headersRange, 3 );
The below example shows how I'm building the query string that will return a bunch of addresses for the search parameters defined in the query string (in this case, Starbuck's)...I'm wondering if it's possible to use the Bing Phonebook API to define a type of entity that you're looking for e.g. Cafe, Gas Station, Software Company, etc...?
function Search(position) {
// note a bunch of this code uses the example code from
// Microsoft for the Phonebook API
var requestStr = "http://api.bing.net/json.aspx?"
// Common request fields (required)
+ "AppId=" + _appId
+ "&Query=starbucks"
+ "&Sources=Phonebook"
// Common request fields (optional)
+ "&Version=2.2"
+ "&Market=en-us"
+ "&UILanguage=en"
+ "&Latitude=" + position.coords.latitude
+ "&Longitude=" + position.coords.longitude
+ "&Radius=100.0"
+ "&Options=EnableHighlighting"
// Phonebook-specific request fields (optional)
// Phonebook.Count max val is 25
+ "&Phonebook.Count=25"
+ "&Phonebook.Offset=0"
// YP = Commercial Entity, WP = Residential
+ "&Phonebook.FileType=YP"
+ "&Phonebook.SortBy=Distance"
// JSON-specific request fields (optional)
+ "&JsonType=callback"
+ "&JsonCallback=?";
$.getJSON(requestStr, function (data) {
SearchCompleted(data);
});
}
I'm not 100% sure, but I don't think the API has a category option. However, I think if you simply included the category in the search query, you would get better results. It's essentially a standard google(bing in this case, obviously) search so you can have it search for any number of terms.
So to find starbucks and define that it should be coffee:
"&Query=starbucks coffee"
Another tip: Unless distance is absolutely important, use:
SortBy=Relevance
This seems to help reduce dumb results
I have confirmed the above answer is right, but I wanted to add that you can sort on both the relevance and the distance