I've created a number of JS scripts similar to below which generate a .tsv download of the webscraped data (this particular example assumes you're on the URL of a repo's Contributors page on Gitlab). Everything outputs fine when I open the .tsv in Microsoft Excel, except that the string 'undefined' appears prepended to every value after the header row in the first column only
How do I edit the script to omit undefined from appearing? Even if it's a simple fix, it will allow me to clean up a bunch of scripts' similar output scraping other websites.
javascript:(function(){
var arr = new Array, i, commitsemail, commitsnum, email, fullname, matchnum;
var regexnum = /.+?(?=commits)/g; var regexemail = /(?<=\().*(?=\))/g;
var glab = document.querySelectorAll('div.col-lg-6.col-12.gl-my-5');
var strings='Full name'+'\t'+'Email'+'\t'+'# of commits'+'\r\n';
var endurl = document.URL.split(/[/]+/).pop(); if (endurl != 'master') {
alert('You are not on the contributors page of a Gitlab repo. Press Esc key, go to URL ending with /master and re-run this bookmarklet'); } else {
for (i = 0; i<glab.length; i++) {
fullname = glab[i].getElementsByTagName('h4')[0].textContent;
commitsemail = glab[i].getElementsByTagName('p')[0].textContent;
commitsnum = [...commitsemail.match(regexnum)];
email = [...commitsemail.match(regexemail)];
arr[i] += fullname + '\t' + email + '\t' + commitsnum;
strings += arr[i]+'\r\n'; }
var pom = document.createElement('a');
var csvContent = strings; var blob = new Blob([csvContent],{type: 'text/tsv;charset=utf-8;'});
var url = URL.createObjectURL(blob); pom.href = url; pom.setAttribute('download','gitlab-contributors.tsv'); pom.click(); }
})();
It's because of the += on the line with arr[i] += fullname + '\t' + email + '\t' + commitsnum;. Change that to an = instead.
Before the assignment, arr[i] is undefined. Maybe you mixed up the syntax for assigning an array entry by index, with appending to an array (arr.push(...)), thinking += would push, but it doesn't. It appends the new value to the current value. And since that line is the first time arr[i] is assigned anything, the current value is undefined.
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.
I am relatively new to JavaScript but I am creating a simple hangman web app in local host. The game runs perfectly, just as I want it. However, I realized that after loading the game at the same time in different tabs, the words change in all instances to the same word. For example, I can run the game in one tab, then another. Then when I go back to the first tab, the word I am trying to guess has changed to the one in the second tab. I don't know how else to describe my issue. Here's my JS and backend code:
JS
function runHangmanInit(){
$.get("/hangmanInit", {}, function(response){ //generates word
term.write("RUNNING HANGMAN.cpp\r\n");
guessedLetters = [];
let json = JSON.parse(response);
console.log(json);
term.write(json["state"] + "\r\n");
term.write("Category: " + json["category"] + "\t");
for(let i = 0; i < json["dashes"]; i++){
term.write(" _");
}
term.write("\r\nGuesses remiaining: "+ json["guesses"] + "\r\n");
term.write(json["message"]);
});
}
function checkLetterTerminal(letter){
$.get("/hangman/" + letter, {}, function(response){
let x = JSON.parse(response);
//console.log(x);
if(x["err"]){
term.write(x["err"]);
}
else{
term.write(x["state"] + "\r\n");
term.write("Category: " + x["category"] + "\t");
for(let i = 0; i < x.dashes.length; i++){
term.write(x.dashes[i] + " ");
}
term.write("\r\nGuesses remiaining: "+ x["guesses"] + "\r\n");
if(x["lettersGuessed"]){ //Incorrect guess
guessedLetters.push(x["lettersGuessed"]);
}
term.write("Letters Guessed: ");
for(let i = 0; i < guessedLetters.length; i++){
term.write(guessedLetters[i] + " ");
}
term.write("\r\n");
}
if(x["message"]){
term.write(x["message"] + "\r\n");
check1 = 0;
term.write("\r\n\x1B[1;3;31mdyaranon#DEKTOP-123:\x1B[0m/mnt/e$ ");
}
else{
term.write("\r\nEnter your guess: ");
}
});
}
Backend (Crow C++)
int main(int argc, char* argv[]){
crow::SimpleApp app;
Game game = Game(1);
CROW_ROUTE(app, "/hangmanInit")([](const request& req, response& res){
nlohmann::json x;
game.init();
x["word"] = game.getWord();
x["state"] = game.board.hangman;
x["category"] = game.categoryName;
x["dashes"] = game.board.dashes.size();
x["guesses"] = 6;
x["message"] = "Enter your guess: ";
res.sendJSON(x);
});
CROW_ROUTE(app, "/hangman/<string>")([](const request &req, response &res, string letter){
nlohmann::json x;
x = game.playGame(letter);
res.sendJSON(x);
});
I included all the relative parts of the code. Any help would be appreciated.
It looks like you're only creating one game instance? So yes, there's only once instance server-side and you're sharing it among all clients.
HTTP is stateless, so you first need a way to identify which client is which when they make requests. This is normally done by a session ID of some sort, which should be randomly generated.
It's common to store this session ID in cookies, but you'll actually want to just use a local variable in your JavaScript when the page loads, since you want to treat each tab differently.
From there, when you make your HTTP requests (which you're doing with $.get()), you need to include this session ID. I suggest putting it in the URL, something like:
/hangman/<some-game-id-here>/<some-route>
Finally, on the server side, you need to track multiple game instances... one for each game going on, and associate them with the session IDs.
I have an application that uses Javascript to parses a XML array that is returned from a Webservice and iterates through it and builds it into a table body. It has been working with no issues until lately.
We had some changes on the database that the Webservice is returning results from in which now there are a few columns that could potentially have blanks or null values.
The Javascript fails to run when it hits a childNode that has a blank or null value.
Below is a snapshot of the browser error:
So my question is how do I handle those blanks so that the Javascript will just build an empty string into the table body and continue iterating through the xml array?
I have tried to build an if statement into the Javascript in the for loop to replace the blank or null value with '', but I'm not sure it's going to be doable with the way my table body is being built.
for (i = 0; i < x.length; i++) {
tbody += "<tr><td class=col1>" +
x[i].getElementsByTagName("CheckInDate")[0].childNodes[0].nodeValue +
"</td><td class=col2>" +
x[i].getElementsByTagName("CheckOutDate")[0].childNodes[0].nodeValue +
"</td><td class=col3>" +
x[i].getElementsByTagName("CheckInOut")[0].childNodes[0].nodeValue +
"</td><td>" +
x[i].getElementsByTagName("address")[0].childNodes[0].nodeValue +
"</td><td>" +
x[i].getElementsByTagName("names")[0].childNodes[0].nodeValue +
"</td><td>" +
x[i].getElementsByTagName("companyName")[0].childNodes[0].nodeValue +
"</td><td>" +
x[i].getElementsByTagName("Name")[0].childNodes[0].nodeValue.substr +
"</td><td>" +
x[i].getElementsByTagName("contactPhoneNum")[0].childNodes[0].nodeValue +
"</td ></tr >";
}
Being as how this is an existing application I don't want to rebuild all the functions that build tables using this method so I hope there is an easy solution to this that I'm not seeing.
The problem is that .childNodes will be null if the XML element doesn't have any content (any text nodes). You seem to have a lot of repeating code, to fix this you can create a function to get the content of an XML node with a specific tag name if it has any content or return an empty string.
Here is an example:
function getElementContent(element, tagName) {
const e = element.getElementsByTagName(tagName);
if (e && e.childNodes && e.childNodes.length) {
return e[0].childNodes[0].nodeValue
}
return '';
}
const tbody = x.map(e => `<tr><td class=col1>${getElementContent(e, 'CheckInDate')}</td><td class=col2>${getElementContent(e, 'CheckInDate')}..... `).join('');
I am making an image for my webpage through javascript like so:
photoHTMLString = '<li class = "SliderPhoto"><img src = "' + ImageArray[x].src_small + '" size = "thumb" onclick = "ShowImagePopUP(' + ImageArray[x].src_big + ')" class = "FacebookSliderPhoto"/></li>';
Whenever I try and click a photo go into ShowImagePopUP I get this error:
missing ) after argument list
[Break On This Error] ShowImagePopUp(http://a8.sph...389_84095143389_5917147_2636303_n.jpg)
It doesn't look like I am missing any ')'s so I am lost on the error.
Any suggestions?
You need to wrap the contents of ShowImagePopUP in quotes:
"ShowImagePopUp(\'' + ImageArray[x].src_big + '\')"
Which should render as:
ShowImagePopUp('http://a8.sph...389_84095143389_5917147_2636303_n.jpg')
^ note the quote here
Example: http://jsfiddle.net/V23J6/1/
try
photoHTMLString = '<li class = "SliderPhoto"><img src = "'
+ ImageArray[x].src_small
+ '" size = "thumb" onclick = "ShowImagePopUP(\"'
+ ImageArray[x].src_big + '\")" class = "FacebookSliderPhoto"/></li>';
should do the trick and solve your problem leaving intact the uglyness of you code
A function like this one should be a bit readable and ready to use...
function slideElement(image){
var li=document.createElement('li');
var img=document.createElement('img');
li.appendChild(img);
li.setAttribute('class','SliderPhoto');
img.setAttribute('class','FacebookSliderPhoto');
img.setAttribute('size', 'thumb');
img.setAttribute('src', image.src_small);
img.setAttribute('onclick', function(){showImagePopUP(image.src_big);});
return li;
}
The value in ImageArray[x].src_big needs to be quoted.
Try to avoid building HTML by mashing strings together. Using a DOM builder gives code that is much easier to debug.
You'd probably be better off writing this so the function computes the large URI based on the small URI rather than having it hard coded.
Here's some general advice, build up the strings into intermediate variables and then assemble it at the end. You can then use the debugger to find out where you're getting your ' or "s unbalanced. When you have it all built you can coalesce it into a single line if you want or leave it with the intermediate variables.