Im trying to use app-script newConditionalFormatRule to set color of the cell based on other cell.
Lets say i have this:
A1 = 40
B1 = 90
then i would like A1 to get the color green, since its lower than B1.
I want B1 to get red, since its higher than A1.
Is there a way so i can create this behaviour in a rulset?
like:
var rule3 = SpreadsheetApp.newConditionalFormatRule()
.//check other cell if greater thing.
.setBackground("#EEED09")
.setRanges([range])
.build();
This example creates a conditional formatting rule that sets a gradient of colors from green to red for values in the range. The lowest value is colored green, the highest is red.
function CFtest2() {
var theSheet = SpreadsheetApp.getActive();
var area = theSheet.getRange('A1:B1');
var newRule = SpreadsheetApp.newConditionalFormatRule();
newRule
.setRanges([area])
.setGradientMinpoint('#00FF00')
.setGradientMidpointWithValue('#FFFFFF', SpreadsheetApp.InterpolationType.PERCENTILE, '50')
.setGradientMaxpoint('#FF0000')
.build();
theSheet.getActiveSheet().setConditionalFormatRules([newRule]);
};
Related
I'm making a sheet with details about a bunch of fictional characters, and one column I want to have is their height. I would also really like to use Conditional Formatting with a Color Scale to color-code the tallest and shortest characters, and everything in between.
Unfortunately, I live in the US, and am used to height expressed in feet and inches (e.g. 5'10''), which Google Sheets of course does not recognize as a number. Is there any way to remedy this, besides writing everything in terms of just inches (e.g. 60), such that I could apply conditional formatting directly to the column?
I've tried different formats (e.g. 5'10), and I considered having a hidden column with just the inch value and have conditional formatting work off of that row (doesn't work with Color Scale as far as I can tell, since you can't input a custom formula). One thought I had is somehow formatting things as an improper fraction with a denominator of 12, but hiding the denominator? But I have no idea how that would work. I've Googled as best I can, but I haven't found anything (everything's just about changing row height, which makes sense in hindsight).
I understand that you have two goals in mind. First of all, you should decide which unit length to use for managing heights. I have chosen inches, but you could work with feet if you need. This will simplify the scenario and will allow you to work easily with the data, but you could always create a function that translates inches to the foot/inches combo in order to show the data to a third party. This is the example table that I will use:
And this is my code, I will explain it at the bottom:
function main() {
var sheet = SpreadsheetApp.getActiveSheet();
var data = sheet.getDataRange().getValues();
data = sortTable(data);
sheet.getDataRange().setValues(data);
for (var i = 1; i < data.length; i++) {
data[i][2] = gradient(data.length, i);
}
for (var i = 1; i < data.length; i++) {
sheet.getRange(i, 2).setBackground("#" + data[i][2][0] + data[i][2][1] +
data[i][2][2]);
}
}
function sortTable(data) {
data.sort(function(a, b) {
return b[1] - a[1];
})
return data;
}
function gradient(arraySize, position) {
var relativePosition = position / arraySize;
var topColor = [parseInt("00", 16), parseInt("7A", 16), parseInt("33",
16)]; // Green
var bottomColor = [parseInt("FF", 16), parseInt("FF", 16), parseInt("FF",
16)]; // White
var positionColor = [0, 0, 0];
for (var i = 0; i < 3; i++) {
positionColor[i] = Math.floor(topColor[i] * (1 - relativePosition) +
bottomColor[i] * relativePosition).toString(16);
}
return positionColor;
}
First of all you have to read the data with a combination of getValues()/setValues(), and once you do that you can sort the table based on height so you can create the gradient later. Please notice how I separated the sorting function for better clarity.
After that you need the gradient color for setBackground(). To do so I developed a simple linear gradient function that calculates the RGB code from the top to the bottom. In my example the gradient fades from green to white, but you can change it. I also separated the gradient script into its own function. At this point you already have the sorted table and its gradient colors, so you only have to use setValues() and you are done. Feel free to leave any comment if you have doubts about this approach. This would be the final result:
UPDATE
Based in your comments I get that you need an imperial height format. For that case, you could use =INT(B2)&"' "&TRIM(TEXT(ROUND(MOD(B2,1)*12*16,0)/16,"# ??/??")&"""") (assuming that B2 contains the height). This approach will use Sheets Formulas to calculate the remainder part of the height, and its expression as an irreducible fraction. This is the final result:
I'm looking for a way to conditionally format a cohort table in Google Sheets so that the colors will change from red (low values) through yellow (medium values) to green (high values) based on the values in each row. Anyone knows if this is possible?
Also, choosing the "Color scale" option in conditional formatting menu doesn't work because it colors the table based on the values of the full table, not each row individually.
I can use that option only if I apply it to each row individually, but my dataset has thousands of entries so that doesn't work for me.
Example table: https://docs.google.com/spreadsheets/d/1gpLMdgfs10Flt-VTtsc68E3Feju2H8UQhnai-R_9b3k/copy
Thanks in advance you guys are the greatest!
I wrote a simple script in Apps Script to apply the formatting to every row. It achieves the gradient per row that you want.
Example:
function applyColorGradientConditionalFormat(min = '#FF0000', mid = '#FF9900', max = '#00FF00') {
const sheet = SpreadsheetApp.getActiveSheet();
const lastCol = sheet.getLastColumn();
const conditionalFormatRules = sheet.getConditionalFormatRules();
// Build this conditional rule for all rows in sheet
for (let i = 2; i <= sheet.getLastRow(); i++) {
let range = sheet.getRange('R' + i + 'C2:R' + i + 'C' + lastCol);
let rule = SpreadsheetApp.newConditionalFormatRule()
.setRanges([range])
.setGradientMinpoint(min)
.setGradientMidpointWithValue(mid, SpreadsheetApp.InterpolationType.PERCENT, '50')
.setGradientMaxpoint(max)
.build()
conditionalFormatRules.push(rule);
}
// Apply all conditional rules built to the sheet
sheet.setConditionalFormatRules(conditionalFormatRules);
};
// Easy improvements: Create a menu to build all conditional formats manually
// or setup triggers to do it automatically
For your sample sheet this results in the following table:
Useful documentation:
Class ConditionalFormatRuleBuilder
getConditionalFormatRules()
non scripted:
=(B2:M2=MAX($B2:$M2))*($B2:$M2<>"")
=(B2:M2=MIN($B2:$M2))*($B2:$M2<>"")
I have a google sheet that I want to add a conditional formatting, but the merged cells kinda of get in the way.
In my spreadsheet, I have the columns B,C,D and E where i'm using a formula in column B for when columns C,D and E are "done" column B gets a green background. The problem is, for some rows, columns D and E are merged.
This is the formula I'm using in column B
=and(C:C="done",D:D="done")
My desired result is: When the columns D and E are not merged, column B only gets the green background if columns C,D and E are "done", or else, it stays blank.
When D and E are Merged : B only gets green background if C and DE are "done", or else, stays blank.
Thanks in advance!!
If E always has a non-empty value when D and E aren't merged, you can use that to check for when E is blank using ISBLANK(E:E):
=AND(C:C="done", D:D="done", OR(E:E="done", ISBLANK(E:E)))
Otherwise, there isn't a way to test if a cell is part of a merged range without using a custom function, which is far less efficient, but could technically work with the assistance of a new helper column (e.g. column Z):
/**
* #customfunction
*/
function ISMERGED(cellAddress) {
var cell = SpreadsheetApp.getActiveSheet().getRange(cellAddress);
return cell.isPartOfMerge();
}
Where column Z (or whatever you choose for your helper) has the following in every relevant row (assuming data starts in row 1):
=ISMERGED(ADDRESS(ROW(E1), COLUMN(E1)))
=ISMERGED(ADDRESS(ROW(E2), COLUMN(E2)))
=ISMERGED(ADDRESS(ROW(E3), COLUMN(E3)))
...and so on.
And then for formatting B, use:
=AND(C:C="done", D:D="done", OR(E:E="done", $Z:$Z))
WARNING: If you toggle between merging and unmerging, Z wont contain the correct values immediately, though, because custom functions only re-compute when input changes (and the address of the cell wont in this case).
Update
Here's how you could compute your helper for the whole column downward, by row, by just putting the value in the topmost cell (ex: =ISMERGED("E:E") in cell Z1):
/**
* #customfunction
*/
function ISMERGED(rangeAddress) {
var range = SpreadsheetApp.getActiveSheet().getRange(rangeAddress);
var numCols = range.getNumColumns();
var numRows = range.getNumRows();
var result = [];
for (var i=0; i < numRows; i++) {
var rowRange = range.offset(i, 0, 1, numCols);
result.push(rowRange.isPartOfMerge());
}
return result;
}
best you can get is:
=OR(AND(C:C="done", D:D="done", E:E="done"),
AND(C:C="done", D:D="done", ISBLANK(E:E)))
which will work unless 4th row:
but you could pre-populate the whole E column with =CHAR(1) and then:
How my sheet works
I'm making a spreadsheet to show how much parts I have. By using a dropdown, am I able to show that I created a product. With conditional formatting I am showing that having 0 items isn't an issue when the product is created. Created products with 0 items change from red to purple. Purple means it doesn't matter to have 0 items from this product.
My issue
My issue starts with my dropdown. If I merge cells, The value will go into the upperleft cell. This means other cells inside the merged cell are blank. This gives me a problem with conditional formatting.
My conditional formatting code example:
=if($D2=0;$E2="Created")
I have to change this code for every cell because of the condition combined with a dropdown. Having more than 250 rows would be inhumanly hard to do by hand.
My questions
Are there ways to give all cells of a merged cell the value of the combined cell in an efficient way?
Is there a better way to make my conditional formatting code applyable to merged cells?
This is my sheet
Product items collected sheet link (Shows the problem and solution!)
Product items collected sheet image (Version 1)
Product items collected sheet image (Version 2)
At the heart of this question is the operation of merged cells. When a cell is merged, say over several rows, only the cell at the top left of the merged cell can contain data, respond to conditional formatting, and so on. In a manner of speaking the other cells cease to exist and values CANNOT be assign to them.
The questioner asks:
Q: Are there ways to give all cells of a merged cell the value of the combined cell in an efficient way?
A: No. Not just in an "efficient" way; it's just not possible.
Q: Is there a better way to make my conditional formatting code applicable to merged cells?
A: No and yes ;)
No. In so far as a merged cell is concerned, everything is driven by the value in the top cell of the merged range. There are no other options for the "rest" of the merged cell.
Yes. I'd create a "helper" cells in Column F as in this screenshot
The code to achieve this is dynamic - it will automatically adapt to adding more products, more items, etc.
The logic is fairly simple: Start in F2, test whether E2 has a value (that is, is it the top of the merged cell?). If yes, then assign the value of E2 to F2 AND put that value in a variable for the following cells. If no, the cell in Column E must be part of a merged cell, so assign the value for Column F to the variable that was saved earlier.
function so5270705902() {
// basic declarations
var ss = SpreadsheetApp.getActiveSpreadsheet();
// note this is going to work on the second sheet in the spreadsheet - this can be edited.
var sheet = ss.getSheets()[1];
// Column B contains no merged cells, and always contains data (it is the BOM for the Products).
// so we'll use it to established the last row of data.
var Bvals = sheet.getRange("B1:B").getValues();
var Blast = Bvals.filter(String).length;
// Row 1 is a header row, so data commences in Row 2 - this can be edited
var dataStart = 2;
// Logger.log("the last row in column D = "+Blast);// DEBUG
// set up to loop through the rows of Column F
var mergedcellvalue = "";
for (i = dataStart; i < (Blast + 1); i++) {
// set the range for the row
var range = sheet.getRange(i, 6);
//Logger.log("row#"+i+" = "+range.getA1Notation()); DEBUG
// get the value in column E
var ECell = range.offset(0, -1);
var ECellVal = ECell.getValue();
//Logger.log("offsetrange#"+i+" range value = "+ECellVal);
//Logger.log("Column E, row#"+i+", value = "+ECell.getA1Notation()+" range value = "+ECellVal);//DEBUG
// when a row is merged, on the top row contains any data
// so we'll evaluate to see whether there is any value in this row in Column E
if (ECell.isBlank()) {
//Logger.log("ECell is blank. We're in the middle of the Merged Cell"); ??DEBUG
// Set the value to the lastes value of "mergedcellvalue"
range.setValue(mergedcellvalue);
} else {
//Logger.log("ECell has a value. We're at the top of the merged cell");//DEBUG
// paste the ECellVal into this range
range.setValue(ECellVal);
// Update the "mergedcellvalue" variable so that it can be applied against lower cells of this merged cell
mergedcellvalue = ECellVal;
} // end of the if isblank
} // end of the loop through column F
}
UPDATE 22 October 2018
For development purposes, I used a small range of only 14 rows in Column E. However the questioner's data covers over 250 rows, so I expanded development testing to cover 336 rows (yeah, I know, but I was copy/pasting and I ended up with 336 and was too lazy to delete any rows. OK?). I found that the code took over 81 seconds to process. Not good.
The primary reason (about 80 seconds worth) for the long processing time is that there is a getValue statement within the loop - var ECellVal = ECell.getValue();. This costs about 0.2 seconds per instance. Including getValue in a loop is a classic performance mistake. My bad. So I modified the code to get the values of Column E BEFORE the loop
var Evals = sheet.getRange("e2:E").getValues();.
I was surprised when the execution time stayed around the same mark. The reason was that the isBlank evaluation - if (ECell.isBlank()) { which previously took no time at all, was now consuming #0.2 second per instance. Not good++. So after searching Stack Overflow, I modified this line as follows:
if (!Evals[(i-dataStart)][0]) {.
Including setValues in a loop is also asking for trouble. An option would have been to write the values to an array and then, after the loop, update the Column E values with the array. However in this case, the execution time doesn't seem to have suffered and I'm leaving the setValues inside the loop.
With these two changes, total execution time is now 1.158 seconds. That's a percentage reduction of , um, a LOT.
function so5270705903() {
// basic declarations
var ss = SpreadsheetApp.getActiveSpreadsheet();
// note this is going to work on the second sheet in the spreadsheet - this can be edited.
var sheet = ss.getSheets()[2];
// Column B contains no merged cells, and always contains data (it is the BOM for the Products).
// so we'll use it to established the last row of data.
var Bvals = sheet.getRange("B1:B").getValues();
var Blast = Bvals.filter(String).length;
// Row 1 is a header row, so data commences in Row 2 - this can be edited
var dataStart = 2;
// Logger.log("the last row in column D = "+Blast);// DEBUG
// set up to loop through the rows of Column F
var mergedcellvalue = "";
// get the values for Column E BEFORE the loop
var Evals = sheet.getRange("e2:E").getValues();
for (i = dataStart; i < (Blast + 1); i++) {
// set the range for the row
var range = sheet.getRange(i, 6);
//Logger.log("row#"+i+" = "+range.getA1Notation()); DEBUG
// get the value in column E
var ECell = range.offset(0, -1);
var ECellVal = Evals[(i - dataStart)][0];
//Logger.log("Column E, row#"+i+", value = "+ECell.getA1Notation()+" range value = "+ECellVal);//DEBU
// when a row is merged, on the top row contains any data
// so we'll evaluate to see whether there is any value in this row in Column E
// instead is isblank, which was talking 0.2 seconds to evaluate, this if is more simple
if (!Evals[(i - dataStart)][0]) {
//Logger.log("ECell is blank. We're in the middle of the Merged Cell"); //DEBUG
// Set the value to the lastes value of "mergedcellvalue"
range.setValue(mergedcellvalue);
} else {
//Logger.log("ECell has a value. We're at the top of the merged cell");//DEBUG
// paste the ECellVal into this range
range.setValue(ECellVal);
// Update the "mergedcellvalue" variable so that it can be applied against lower cells of this merged cell
mergedcellvalue = ECellVal;
} // end of the if isblank
} // end of the loop through column F
}
UPDATE 3 March 2019
The questioner made his final changes to the code. This code is the final solution.
function reloadCreatedCells() {
// Basic declarations.
var ss = SpreadsheetApp.getActiveSpreadsheet();
// Note this is going to work on the second sheet in the spreadsheet - this can be edited.
var sheet = ss.getSheets()[1];
// Column B contains no merged cells, and always contains data (it is the BOM for the Products).
// so we'll use it to established the last row of data.
var D_vals = sheet.getRange("D1:D").getValues();
var D_last = D_vals.filter(String).length;
// First row with data.
var dataStart = 2;
// Set up to loop through the rows of Column H - K.
var mergedcellvalue = "";
// Get the values for Column H - K BEFORE the loop.
var H_K_vals = sheet.getRange("H2:K").getValues();
// How many people we have.
var people = 4;
// The first vertical row.
var rowStart = 12;
// Horizontal rows.
for (var h = 0; h < people; h++) {
// Vertical rows.
for (var v = dataStart; v < D_last; v++) {
// Set the range for the row.
var range = sheet.getRange(v, rowStart + h);
// Logger.log(range.getA1Notation()); //DEBUG
// Get the value in column H - K.
var H_K_Cell = range.offset(0, -people);
// Adding Created and not created values inside L - O.
var H_K_CellVal = H_K_vals[(v - dataStart)][h];
// Logger.log(H_K_Cell.getA1Notation() + ': ' + H_K_CellVal); //DEBUG
// When a row is merged, the value is only inside the top row.
// Therefore, you need to check if the value is empty or not.
// If the value is empty. Place the top value of the merged cell inside the empty cell.
if (!H_K_vals[(v - dataStart)][h]) {
// Logger.log(H_K_Cell.getA1Notation() + ": is blank. We're below the top cell of the merged cell."); //DEBUG
// Set the value to the top cell of the merged cell with "mergedcellvalue".
range.setValue(mergedcellvalue);
} else {
// Logger.log(H_K_Cell.getA1Notation() + ": has a value. We're at the top of the merged cell."); //DEBUG
// Paste the H_K_CellVal into this range.
range.setValue(H_K_CellVal);
// Update the "mergedcellvalue" variable, so that it can be applied against lower cells of this merged cell.
mergedcellvalue = H_K_CellVal;
} // end of the if isblank.
} // End of the vertical row loop.
} // End of the horizontal row loop.
}
I'm using a function to count the number of colored cells, but when they change color (not value), the spreadsheet doesn't recognize a value change and my formula doesn't update with the new number of colored cells.
To manually force an update, I have to add a character to that cell, then remove it, so that the sheet will recognize a value change and recount the number of colored cells. Is there a way to automate this and force the sheet to add and remove a character in order to force an update?
Hopefully, this is clear enough to be answerable.
You can simply use onEdit function if there is any data update on the cells. But for your case, I have tested below scripts works good, but I am still confused it is the best practice of coding, because I could not find any other options to solve your question.
//Below function will count the number of cells with the specified background color
function countWhereBackgroundColorIs(color, rangeSpecification) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheets()[0];
var range = sheet.getRange(rangeSpecification);
var bgColors = range.getBackgrounds();
var k = 0;
for (var i in bgColors) {
for (var j in bgColors[i]) {
if(bgColors[i][j] == color)
k = k +1;
}
}
return k;
}
//Below function will append a space" " on the cell A1 data and then remove the appended char. This is for updating the range. Make sure that at least a cell mentioned in above function countWhereBackgroundColorIs() is getting updated. Here Used 'A1'
function updateSheet()
{
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheets()[0];
var cell = sheet.getRange("A1");
var d = cell.getValue();
cell.setValue(d+" ");
SpreadsheetApp.flush();
cell.setValue(d);
SpreadsheetApp.flush();
}
//creating a time based trigger to execute the updateSheet() function for every one min. Minimum time is 1min. This trigger creation is a one time activity and this can be done in GUI also Resources>All your triggers.
function createTimeDrivenTriggers() {
ScriptApp.newTrigger('updateSheet')
.timeBased()
.everyMinutes(1)
.create();
}