Search code examples
google-sheetsgs-conditional-formatting

Google Spreadsheet dynamic conditional formatting with merged dropdown


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

  1. Are there ways to give all cells of a merged cell the value of the combined cell in an efficient way?
  2. 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)


Solution

  • 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


    Helper Column before and after


    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.
    }