I posted a similar question earlier (Retrieving R object attributes in JavaScript). In that earlier post, I oversimplified my MWE, and so the answer I rewarded unfortunately does not really apply to my real problem. Here, I am showing why I may need to retrieve R object attributes in JavaScript (unless there is another option that I am not aware of).
I have a 5-variable dataset with 100 observations. I used hexagon binning and created a scatterplot matrix. Each of the 10 scatterplots has somewhere between 12-18 hexagons. In order to save the rows of the 100 observations that are in each of the hexagon bins for all 10 scatterplots, I used the base::attr function in R. In the code below, this is done at:
attr(hexdf, "cID") <- h#cID
I am trying to create an interactive R Plotly object of the hexagon binning so that if a user were to click on a given hexagon bin (regardless of which scatterplot), they would obtain the rows of the 100 observations that were grouped into that bin. I have part of this goal completed. My MWE is below:
library(plotly)
library(data.table)
library(GGally)
library(hexbin)
library(htmlwidgets)
set.seed(1)
bindata <- data.frame(ID = paste0("ID",1:100), A=rnorm(100), B=rnorm(100), C=rnorm(100), D=rnorm(100), E=rnorm(100))
bindata$ID <- as.character(bindata$ID)
maxVal = max(abs(bindata[,2:6]))
maxRange = c(-1*maxVal, maxVal)
my_fn <- function(data, mapping, ...){
x = data[,c(as.character(mapping$x))]
y = data[,c(as.character(mapping$y))]
h <- hexbin(x=x, y=y, xbins=5, shape=1, IDs=TRUE, xbnds=maxRange, ybnds=maxRange)
hexdf <- data.frame (hcell2xy (h), hexID = h#cell, counts = h#count)
attr(hexdf, "cID") <- h#cID
p <- ggplot(hexdf, aes(x=x, y=y, fill = counts, hexID=hexID)) + geom_hex(stat="identity")
p
}
p <- ggpairs(bindata[,2:6], lower = list(continuous = my_fn))
pS <- p
for(i in 2:p$nrow) {
for(j in 1:(i-1)) {
pS[i,j] <- p[i,j] +
coord_cartesian(xlim = c(maxRange[1], maxRange[2]), ylim = c(maxRange[1], maxRange[2]))
}
}
ggPS <- ggplotly(pS)
myLength <- length(ggPS[["x"]][["data"]])
for (i in 1:myLength){
item =ggPS[["x"]][["data"]][[i]]$text[1]
if (!is.null(item))
if (!startsWith(item, "co")){
ggPS[["x"]][["data"]][[i]]$hoverinfo <- "none"
}
}
ggPS %>% onRender("
function(el, x, data) {
el = el;
x=x;
var data = data[0];
console.log(el)
console.log(x)
console.log(data)
myLength = Math.sqrt(document.getElementsByClassName('cartesianlayer')[0].childNodes.length);
console.log(myLength)
el.on('plotly_click', function(e) {
console.log(e.points[0])
xVar = (e.points[0].xaxis._id).replace(/[^0-9]/g,'')
if (xVar.length == 0) xVar = 1
yVar = (e.points[0].yaxis._id).replace(/[^0-9]/g,'')
if (yVar.length == 0) yVar = 1
myX = myLength + 1 - (yVar - myLength * (xVar - 1))
myY = xVar
cN = e.points[0].curveNumber
split1 = (x.data[cN].text).split(' ')
hexID = (x.data[cN].text).split(' ')[2]
counts = split1[1].split('<')[0]
console.log(myX)
console.log(myY)
console.log(hexID)
console.log(counts)
})}
", data = pS[5,2]$data)
This creates an image as shown below:
As an example, if I click on the hexagon highlighted in the green box, I can determine which subplot it occurred in ("myX" and "myY"), the ID of the hexagon clicked ("hexID"), and the number of observation points that were binned into that hexagon ("counts"). For this particular hexagon, myX=5, myY=2, hexID=39, and counts=1. So, the user just clicked on hexagon with ID39 in the scatterplot on the fifth row and second column and there should be 1 data point that it binned.
If I leave the onRender() function, and simply type into R the following code:
myX <- 5
myY <- 2
hexID <- 39
obsns <- which(attr(pS[myX,myY]$data, "cID")==hexID)
dat <- bindata[obsns,]
Then, I can obtain the row of the data frame that contains the one observation that was binned into that clicked hexagon:
> dat
ID A B C D E
95 ID95 1.586833 -1.208083 1.778429 -0.1101588 3.810277
My problem is simply in this last step. I am unable to figure out how to use the base::attr() function from within the onRender() function in order to obtain the "obsns" object. Is there any workaround for this issue, or another possible approach I should consider taking? Thank you for any ideas/advice!
I'm not sure you can access the hex IDs from plotly or whether it keeps this data somewhere so one option is to pass all the data needed for your purpose to the onRender function.
First you could add to your bindata dataframe a column per hexplot, called mX-mY (where you replace mX and mY by their value for each column), that would hold for each observation the hexbin it belongs to for that plot:
for(i in 2:5) {
for(j in 1:4) {
bindata[[paste(i,j,sep="-")]] <- attr(pS[i,j]$data, "cID")
}
}
You can then pass bindata to the onRender function and whever you click on a hexagon in one of the plot, check in the corresponding column in bindata which observations belong to that hexbin:
ggPS %>% onRender("
function(el, x, data) {
myLength = Math.sqrt(document.getElementsByClassName('cartesianlayer')[0].childNodes.length);
el.on('plotly_click', function(e) {
xVar = (e.points[0].xaxis._id).replace(/[^0-9]/g,'')
if (xVar.length == 0) xVar = 1
yVar = (e.points[0].yaxis._id).replace(/[^0-9]/g,'')
if (yVar.length == 0) yVar = 1
myX = myLength + 1 - (yVar - myLength * (xVar - 1))
myY = xVar
cN = e.points[0].curveNumber
split1 = (x.data[cN].text).split(' ')
hexID = (x.data[cN].text).split(' ')[2]
counts = split1[1].split('<')[0]
var selected_rows = [];
data.forEach(function(row){
if(row[myX+'-'+myY]==hexID) selected_rows.push(row);
});
console.log(selected_rows);
})}
", data = bindata)
Related
Been dealing with this code for a couple of days now. What I want to achieve in the spreadsheet is, move some columns of a single row to another row based on some criteria, and leave the older rows clear of content.
To put it other way, I check at which row columns B, C and H (let's call this row N° '3') match exactly with values at columns B, C and N (let's call this row N° '9'). Then I need to move information from J9:N9 to J3:N3.
So this is the code I've come up with. Problem is, at the end no changes are being made to the spreadsheet nor the 'rows' array when I log it.
function reordenarFacturas(){
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName('AP que esperan');
var lastRow = sheet.getLastRow(); //get the last Row with values
var filteredArray = [];
const rows = sheet.getDataRange().getValues(); // get data as an array
const nombresAP = sheet.getSheetValues(3,2,lastRow-2,2); //get columns 2 and 3 as an array
//Clean nombresAP array from special characters so that later filterLogic works correctly (not using filter anymore, but still wanted to clear names list).
for (h=0; h<lastRow-2; h++){ //rows
for(j=0; j<2;j++){ //columns
nombresAP[h][j] = nombresAP[h][j].toString().toUpperCase();
nombresAP[h][j] = nombresAP[h][j].toString().replace(/á/ig, "A");
nombresAP[h][j] = nombresAP[h][j].toString().replace(/é/ig, "E");
nombresAP[h][j] = nombresAP[h][j].toString().replace(/í/ig, "I");
nombresAP[h][j] = nombresAP[h][j].toString().replace(/ó/ig, "O");
nombresAP[h][j] = nombresAP[h][j].toString().replace(/ú/ig, "U");
nombresAP[h][j] = nombresAP[h][j].toString().replace(" ", " ");
}
}
sheet.getRange("B3:C" + lastRow).setValues(nombresAP);
//End of the cleaning section
//NOT IN USE CURRENTLY, SAVED JUST IN CASE
//Filterlogic works as a way to input several parameters to the filter method used below (now erased).
/*
var filterLogic = function(item){ //function to pass filter data later
if (item[1] === apellidoAP && item[2] === nombreAP && item[7] === periodoFacturado){
return true;
}
else{
return false;
}
}
*/
//Here I check whether certain columns have different values. If they do, then I retrieve some values on variables and push them to an array for later use.
for (var h = 2; h<lastRow; h++){ // h starts at 2 because data starts at row N° 3
if (rows[h][9] == '') continue; // If cells on this columns are empty, I don't need the row and can evaluate the next
else if (rows[h][7] != rows[h][13]){ // If this two different cells of the same row have different values, I then
//assign some data to variables, for later filtering other rows with said data
var apellidoAP = rows[h][1];
var nombreAP = rows[h][2];
var periodoInformado = rows[h][7];
var factura = rows[h][9];
var fechaFactura = rows[h][10];
var nroFactura = rows[h][11];
var montoFactura = rows[h][12];
var periodoFacturado = rows[h][13];
filteredArray.push([[apellidoAP], [nombreAP], [periodoFacturado], [factura], [fechaFactura], [nroFactura], [montoFactura], [periodoFacturado]]);
// Here I'm 'cleaning' some fields in the rows where I've found the differences (doesn't seem to be working)
rows[h][9] == '';
rows[h][10] == '';
rows[h][11] == '';
rows[h][12] == '';
rows[h][13] == '';
}
}
//And finally here, I check whether some filteredArray values match some values in the 'rows' array. If they do match, then I replace some more values in rows array with values from filteredArray. This doesn't seem to be working either.
for (p = 2; p<rows.length; p++){
for (r = 0; r<filteredArray.length; r++){
if (rows[p][1] == filteredArray [r][0] && rows[p][2] == filteredArray[r][1] && rows[p][7] == filteredArray[r][2]){
rows[p][9] == filteredArray[r][3];
rows[p][10] == filteredArray[r][4];
rows[p][11] == filteredArray[r][5];
rows[p][12] == filteredArray[r][6];
rows[p][13] == filteredArray[r][7];
}
}
}
//Finally I set rows back at the spreadsheet with all changes made, but no changes are shown in the spreadsheet once the script ends running.
sheet.getRange("A1:U" + lastRow).setValues(rows);
}
```
I'm attempting to build a function in google script that would allow me to code in certain column names (as these will remain constant), and to have said function find that column by the name, and create/insert a custom formula via conditional formatting to highlight duplicates in that column. The formula to find duplicates is =COUNTIF(E:E,E1)>1 (using column E as an example).
So to re-state, what I'm trying to do is:
Have a function that inserts a new conditional format rule to highlight duplicates in a row
Have the formula for the new conditional format be dynamic in case the column moves between data sets (although the column name will be the same)
Have this function insert the conditional format into a column by name, instead of the number
I've tried to find something similar to this on stackoverflow but not finding much luck, just a few posts that are:
Custom Formula in Condition Format Rule Builder Script
Get column values by column name not column index
So this sheet would find the column "WOW":
Desired outcome/Would look like this after the function has ran (Highlight E2 and E2 in the WOW column):
So my attempt at making this code is below, however I'm hoping someone has a better idea as it's not quite working and this seems to be advanced coding (and I'm a newbie)
function conditionalformat1(){
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('sheet1');//need to call the tab name as I have many tabs
var colName = 'WOW' //specific column name to have this function search for
var sheetName = 'sheet1'
var newRule = SpreadsheetApp.newConditionalFormatRule()
.whenFormulaSatisfied('=COUNTIF(E:E,E1)>1')//this should be dynamic instead what it is now...not sure if we can use R1C1 notation?
.setBackground('red')
.setRanges(getColByName)
.build()//similar setup to (https://stackoverflow.com/questions/50911538/custom-formula-in-condition-format-rule-builder-script) #Patrick Hanover & https://developers.google.com/apps-script/reference/spreadsheet/conditional-format-rule-builder#whenFormulaSatisfied(String)
var rules = sheet.getConditionalFormatRules();
rules.push(conditionalformat1);
sheet.setConditionalForatRules(rules);
function getColByName(colName) {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('sheet1');
var colName = 'WOW'
var data = sheet.getRange("1:1000").getValues();//have it search the entire sheet?
var col = data[0].indexOf(colName);
if (col != -1) {
return data[row-1][col];
}
}// via the question & author https://stackoverflow.com/questions/36346918/get-column-values-by-column-name-not-column-index #Leonardo Pina
}//end of conditionalformat1 conditional formatting
Thanks for the help in advance, this will be great to learn how to have functions find columns by name and execute items.
You do not need any code, just use the conditional formatting formula
=AND(a$1="WOW";countif(A$2:A;a2)>1)
Get column number using your current function getColumnByName
Get column letter from the column number
Create a range string from column number
Use the range string in your custom formula and the range variable in conditional formatting
function conditionalformat0() {
const sheetName = 'sheet1';
const colName = 'WOW'; //specific column name to have this function search for
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName); //need to call the tab name as I have many tabs
const c2l = (n) =>
/*Column to Letter: 0 => A*/
n >= 26
? String.fromCharCode(64 + n / 26) + c2l(n % 26)
: String.fromCharCode(65 + n);
const getColByName = (colName, sheet) => {
const data = sheet.getRange(`1:${sheet.getLastColumn()}`).getValues();
const col = data[0].indexOf(colName);
if (col !== -1) return col;
throw new Error('Column not found');
};
const colLetr = c2l(getColByName(colName, sheet));
const rangeA1 = `${colLetr}:${colLetr}`;
const newRule = SpreadsheetApp.newConditionalFormatRule()
.whenFormulaSatisfied(`=COUNTIF(${rangeA1},${colLetr}1)>1`)
.setBackground('red')
.setRanges([sheet.getRange(rangeA1)])
.build();
const rules = sheet.getConditionalFormatRules();
rules.push(newRule);
sheet.setConditionalFormatRules(rules);
}
/*
If you want to find columns by name the best thing to do is to build an object that does it for you so that you don't have to call and additional function every time you need the column name.
First of all you have to know the row that the column names are on. I call this the header row
*/
function getColumnHeaders() {
const ss = SpreadsheetApp.getActive();
const sh = ss.getSheetByName('SheetName');
const hr = 1;//header row
const hA = sh.getRange(hr, 1, 1, sh.getLastColumn()).getValues().flat();
const col = {};//the object that converts column names to numbers
hA.forEach((h, i) => col[h] = i + 1);
/* this gives you column numbers however often what you really want are the column indices so that you can use them to access data in rows of the 2 dimensional arrays like the ones returned by getValues() function in this case you would do it the following way
*/
const idx = {}; //the object that does the conversion for you from column names to indices
hA.forEach((h, i) => idx[h] = i);
/* if you want both then you can do it this way */
hA.forEach((h, i) => col[j] = i + 1; idx[h] = i;);
/* Now if you wish to access data from a spreadsheet with a header row by using header names instead of indexes you can */
const data = sh.getRange(startRow, startCol, sh.getLastRow() - startRow + 1, sh.getLastColumn() - sc + 1).getValues();
data.forEach(r =>
r[idx["column name"]];//this accesses the value in the current row for the column name that you enter and it does not require you to do an additional function call to do it because it's already in the object and most like the object is located in the cpu's cache so this is much faster
)
}
I don't consider this to be a complete answer to the question I just wanted to point out a way to do this that doesn't require function calls because unnecessary function calls inside of your data loops will greatly decrease performance.
I've fixed your code a bit. It should work to some extent. Try it:
function conditionalformat() {
var sheetName = 'Sheet1';
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);
var colName = 'WOW';
var range = sheet.getRange(1,getColByName(colName,sheet),sheet.getLastRow());
var formula = '=COUNTIF(E:E,E1)>1';
var newRule = SpreadsheetApp.newConditionalFormatRule()
.whenFormulaSatisfied(formula)
.setBackground('red')
.setRanges([range])
.build();
var rules = sheet.getConditionalFormatRules();
rules.push(newRule);
sheet.setConditionalFormatRules(rules);
}
function getColByName(colName,sheet) {
var data = sheet.getDataRange().getValues();
var col = data[0].indexOf(colName);
if (col != -1) return col+1;
}
But I don't understand what do you want to do with the formula =COUNTIF(E:E,E1)>1. Should it changing somehow? How?
And just in case, here is the pure JS function (based on Amit Agarwal's solution) to get column letter(s) from a number:
function get_col_letter(column) {
var col_letter = '';
let block = column;
while (block >= 0) {
col_letter = String.fromCharCode((block % 26) + 65) + col_letter;
block = Math.floor(block / 26) - 1;
}
return col_letter;
};
console.log(get_col_letter(0)); // 'A'
console.log(get_col_letter(1)); // 'B'
console.log(get_col_letter(25)); // 'Z'
console.log(get_col_letter(26)); // 'AA'
Update
I've added the option to change letters in formula by given column:
function conditionalformat() {
var sheetName = 'Sheet1';
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);
var colName = 'WOW';
var colNum = getColNum(colName,sheet);
var colLetter = getColLetter(colNum);
var range = sheet.getRange(`${colLetter}1:${colLetter}`);
var formula = `=COUNTIF(${colLetter}:${colLetter},${colLetter}1)>1`;
var newRule = SpreadsheetApp.newConditionalFormatRule()
.whenFormulaSatisfied(formula)
.setBackground('red')
.setRanges([range])
.build();
// sheet.clearConditionalFormatRules(); // remove old formatting
var rules = sheet.getConditionalFormatRules();
rules.push(newRule);
sheet.setConditionalFormatRules(rules);
}
// get number column from its name
function getColNum(colName,sheet) {
var data = sheet.getDataRange().getValues();
var col = data[0].indexOf(colName);
if (col != -1) return col+1;
}
// get letter column from its number
function getColLetter(column) {
var col_letter = '';
let block = column - 1;
while (block >= 0) {
col_letter = String.fromCharCode((block % 26) + 65) + col_letter;
block = Math.floor(block / 26) - 1;
}
return col_letter;
};
There are two ways that i am able to add an auto increment column. By auto-increment, i mean that if column B has a value, column A will be incremented by a numeric value that increments based on the previous rows value.
The first way of doing this is simple, which is to paste a formula like the one below in my first column:
=IF(ISBLANK(B1),,IF(ISNUMBER(A1),A1,0)+1)
The second way i have done this is via a GA script. What i found however is performance using a GA script is much slower and error prone. For example if i pasted values quickly in the cells b1 to b10 in that order, it will at times reset the count and start at 1 again for some rows. This is because the values for the previous rows have not yet been calculated. I assume that this is because the GA scripts are probably run asynchronously and in parallel. My question is..is there a way to make sure each time a change happens, the execution of this script is queued and executed in order?
OR, is there a way i should write this script to optimize it?
function auto_increment_col() {
ID_COL = 1;
var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
//only increment column 1 for sheets in this list
var auto_inc_sheets = SpreadsheetApp.getActiveSpreadsheet().getRangeByName("auto_inc_sheets").getValues();
auto_inc_sheets = auto_inc_sheets.map(function(row) {return row[0];});
var is_auto_inc_sheet = auto_inc_sheets.indexOf(spreadsheet.getSheetName()) != -1;
if (!is_auto_inc_sheet) return;
var worksheet = spreadsheet.getActiveSheet();
var last_row = worksheet.getLastRow();
var last_inc_val = worksheet.getRange(last_row, ID_COL).getValue();
//if auto_inc column is blank and the column next to auto_inc column (col B) is not blank, then assume its a new row and increment col A
var is_new_row = last_inc_val == "" && worksheet.getRange(last_row, ID_COL+1).getValue() != "";
Logger.log("new_row:" + is_new_row + ", last_inc_val:" + last_inc_val );
if (is_new_row) {
var prev_inc_val = worksheet.getRange(last_row-1, ID_COL).getValue();
worksheet.getRange(last_row, ID_COL).setValue(prev_inc_val+1);
}
}
There is my vision of auto increment https://github.com/contributorpw/google-apps-script-snippets/tree/master/snippets/spreadsheet_autoincrement
The main function of this is
/**
*
* #param {GoogleAppsScript.Spreadsheet.Sheet} sheet
*/
function autoincrement_(sheet) {
var data = sheet.getDataRange().getValues();
if (data.length < 2) return;
var indexCol = data[0].indexOf('autoincrement');
if (indexCol < 0) return;
var increment = data.map(function(row) {
return row[indexCol];
});
var lastIncrement = Math.max.apply(
null,
increment.filter(function(e) {
return isNumeric(e);
})
);
lastIncrement = isNumeric(lastIncrement) ? lastIncrement : 0;
var newIncrement = data
.map(function(row) {
if (row[indexCol] !== '') return [row[indexCol]];
if (row.join('').length > 0) return [++lastIncrement];
return [''];
})
.slice(1);
sheet.getRange(2, indexCol + 1, newIncrement.length).setValues(newIncrement);
}
But you have to open the snippet for details because this doesn't work without locks.
I have existing charts that displays data for a full day 12:00am - 12:00am.
Now required to change one chart forward to display 4:00am - 4:00am.
I have managed to shift the x axis labels (.add(4, 'hours')) but the chart data is still in the same position.
How do I shift the charted data forward 4 hours?
Limited scope to change global variables as this will impact other charts.
var getChartSeries = function(response, chart_series_data) {
var lines = response.graph_data.lines;
for (var i=0; i<lines.length; i++) {
var series = lines[i];
var dateFormat = graphDateFormat;
if (chartIntraday) dateFormat = 'HH:mm:ss';
var currSeriesData = [];
for (var j=0; j<series.data.length; j++) {
var row = series.data[j];
var yValue = parseFloat(row[1]);
var point = {
x: moment(row[0], dateFormat).add(4, 'hours').valueOf(),
y: yValue,
displayValue: row[3]
};
currSeriesData.push(point);
}
// Set the series name, legend label, and the line identifier
var name = formatLegendLabel(series.display_name, response);
var label = response.label;
if (response.display_name != undefined && response.display_name != '') label = series.display_name + ' : ' + label;
By default chart adjusts extremes to the provided data. To display these few hours before the first point use xAxis.min property.
Live demo: http://jsfiddle.net/kkulig/xqdqooh9/
API reference: https://api.highcharts.com/highcharts/xAxis.min
I have a line-plot with very little data. A bit like this:
plot_ly(x = c( -2, 0, 1.5 ),y = c( -2, 1, 2.2),
type = 'scatter' ,mode = 'lines+markers') %>%
add_trace(x=c(-1,0.4,2.5),y=c(2, 0, -1),type='scatter',mode='lines+markers')
I want to know whether plotly can display all the hoverinfos of a line at ones. Such that for example when I click on "trace 1" in the legend, I can see it has the points (-1,2), (0.4,0), (2.5,-1) beside the corresponding point. Couldn't find anything so far.
You can trigger hover events with Plotly.Fx and add the necessary Javascript code by using htmlwidgets.
Fx.hover needs a curveNumber and curvePoint (the n-th of your trace) as input parameters. If you want to trigger events for multiple points, you need to pass an array of objects.
library("plotly")
library("htmlwidgets")
p <- plot_ly(x = c(-2, 0, 1.5 ),
y = c(-2, 1, 2.2),
type = 'scatter',
mode = 'lines+markers') %>%
add_trace(x = c(-1, 0.4, 2.5),
y = c(2, 0, -1),
type = 'scatter',
mode = 'lines+markers') %>%
layout(hovermode = 'closest')
javascript <- "
var myPlot = document.getElementsByClassName('plotly')[0];
myPlot.on('plotly_click', function(data) {
var hover = [];
for (var i = 0; i < data.points[0].data.x.length; i += 1) {
hover.push({curveNumber: data.points[0].curveNumber,
pointNumber: i});
}
Plotly.Fx.hover(myPlot, hover);
});"
p <- htmlwidgets::prependContent(p, onStaticRenderComplete(javascript), data = list(''))
p
Note: Plotly.Fx will be deprecated.