Utilizing Apps Script for tallying background colors in Google Sheets

Sorry for the lengthy explanation! Let me know if you need more details or examples from the JavaScript file/spreadsheet I'm working with.

Situation:

I'm facing challenges with Google Apps Script (GAS) while trying to count the number of background colors in a child's schedule that I've developed.

I have a basic week view divided into 30-minute intervals from 5:30 am to 7:30 pm. Cells are colored differently based on whether the child is at home, different events, etc. Here's an example: https://i.sstatic.net/10J0j.png

My goal is to calculate the number of purple, orange, and green cells. I plan to divide these counts by 2 to determine the hours spent at each location during the week.

Steps Followed:

An important aspect is that I must use merged cells within the target range for counting.

This resource was quite helpful: Script Google Sheets that will count the amount of cells with a specific background colour

This led me to create a custom function like this:

//Gets the count of cells with a particular colour within the range. Merged cells count as 1
function countColouredCells(countRange,colorRef) {
  var activeRange = SpreadsheetApp.getActiveRange();
  var activeSheet = activeRange.getSheet();
  var formula = activeRange.getFormula();

  var rangeA1Notation = formula.match(/\((.*)\,/).pop();
  var range = activeSheet.getRange(rangeA1Notation);
  var bg = range.getBackgrounds();
  var values = range.getValues();
  
  var colorCellA1Notation = formula.match(/\,(.*)\)/).pop();
  var colorCell = activeSheet.getRange(colorCellA1Notation);
  var color = colorCell.getBackground();
  
  var count = 0;
  
  for(var i=0;i<bg.length;i++)
    for(var j=0;j<bg[0].length;j++)
      if( bg[i][j] == color )
        count=count+1;
  return count;
};

//Usage in cell formula: "=countcolouredcells($E$11:$K$39,L46)" where $E$11:$K$39 is the range to count the occurances of cells with the same background as L46

However, this method considers merged cells as 1, whereas I want it to count the actual number of cells covered by the merge. For example, in the image above, the green "School" cell on Monday should be counted as 14, not 1.

Further investigation led me to develop two functions borrowed from solutions on StackOverflow, which can break apart and remerge merged cells.

var mergedRanges;

function breakApartRange(breakRange){
  var activeRange = SpreadsheetApp.getActiveRange();
  var activeSheet = activeRange.getSheet();
  var formula = activeRange.getFormula();
  var fullRange = activeSheet.getRange(breakRange);
  
  //break merged cells
  mergedRanges = fullRange.getMergedRanges();
  mergedRanges.forEach(range => range.setValue(range.breakApart().getValue()));
};


function reMergeRange(breakRange){
  var activeRange = SpreadsheetApp.getActiveRange();
  var activeSheet = activeRange.getSheet();
  var formula = activeRange.getFormula();
  var fullRange = activeSheet.getRange(breakRange);
  
  //re merge cells
  mergedRanges.forEach(range => range.merge());
};

A limitation is that the breakApart() method cannot be directly called within a custom function used in a formula. To overcome this, I created a custom menu item triggered by the onOpen method as a workaround.

//Adds a menu item to run the function:
// The onOpen function is executed automatically every time a Spreadsheet is loaded
function onOpen() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var menuEntries = [];

  menuEntries.push({name: "Calculate Hourly Split", functionName: "calcHourlySplitByColour"});

  ss.addMenu("CustomFunctions", menuEntries);
};

To make this work, I had to use fixed references for the range and color, but it serves my purpose. Now, I have a custom function named calcHourlySplitByColour, which is invoked through the menu item, and does the following:

  1. Breaks apart the merges
  2. Calls another custom function (countColouredCells) to count the colored cells
  3. Stores the count in a separate cell for viewing
  4. Merges the cells back to their original state

Individually, each step works fine (the breaking apart and remerging tested successfully when triggered from the custom menu item, and countColouredCells seems to work when invoked from a formula in the sheet.)

Challenge Faced:

The issue arises when I try to integrate everything, especially when attempting to call countColouredCells via the custom menu item. To make it functional, I need to pass the ranges into countColouredCells statically, but encounter difficulties when executing the pop() method:

//Gets the count of cells with a particular colour within the range. Merged cells count as 1
function countColouredCells2(countRange,colorRef) {
 
  var activeRange = SpreadsheetApp.getActiveSpreadsheet().getRangeByName(countRange);
  var activeSheet = activeRange.getSheet();
  var formula = activeRange.getFormula();

  //Testing code just to see if the activeRange was null - it wasn't, this returned "1st PopCalibri" in the alert
  SpreadsheetApp.getUi().alert("1st Pop" + activeRange.getFontFamily());
  
  //This is the line that causes the error (i.e. I never see an alert for "2nd Pop"):
  var rangeA1Notation = formula.match(/\((.*)\,/).pop();
  var range = activeSheet.getRange(rangeA1Notation);
  var bg = range.getBackgrounds();
  var values = range.getValues();
  
  SpreadsheetApp.getUi().alert("2nd Pop");
  var colorCellA1Notation = formula.match(/\,(.*)\)/).pop();
  var colorCell = activeSheet.getRange(colorCellA1Notation);
  var color = colorCell.getBackground();
  
  SpreadsheetApp.getUi().alert("Post 2nd Pop");
  var count = 0;
  
  for(var i=0;i<bg.length;i++)
    for(var j=0;j<bg[0].length;j++)
      if( bg[i][j] == color )
        count=count+1;
  return count;
};

This is a draft version, a modification of my initial countColouredCells function. While I haven't handled the colorRef parameter yet, the problematic line is:

    var rangeA1Notation = formula.match(/\((.*)\,/).pop();

which pertains only to the first parameter, countRange. The error message received is as follows: https://i.sstatic.net/WOGeo.png

https://i.sstatic.net/dAZLv.png

You can access an example of the spreadsheet here. In case you're unable to view the apps script for a shared sheet, find the related content here.

Answer №1

I see your objective is outlined as follows.

  • You are seeking to extract the number of background colors of all cells from a sheet, including merged cells, using Google Apps Script.
  • Based on the Spreadsheet image provided, you aim to retrieve 76, 47, 13, 67 for
    '#d9d2e9', '#fce5cd', '#cfe2f3', '#d9ead3'
    , respectively.

In this scenario, consider the following solution. The key points of this sample script are:

  • Initially, the background colors are retrieved from all cells, accounting for merged cells where all background colors, except for the 1st cell, are identified as "#ffffff".
  • Subsequently, the extracted background colors of all cells are updated by detecting merged cells through the use of getMergedRanges().

Following this process enables obtaining the background colors of all cells. When incorporated into a sample script, it translates into the code below.

Sample script:

function myFunction() {
  const sheetName = "Sheet1"; // Specify your sheet name here.
  
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);
  const range = sheet.getRange("B2:H30");
  const offsetR = range.getRow() - 1;
  const offsetC = range.getColumn() - 1;
  const backgrounds = range.getBackgrounds();
  range.getMergedRanges().forEach(r => {
    const startRow = r.getRow() - 1 - offsetR;
    const endRow = startRow + r.getNumRows() - 1;
    const startCol = r.getColumn() - 1 - offsetC;
    const endCol = startCol + r.getNumColumns() - 1;
    const base = backgrounds[startRow][startCol];
    for (let r = startRow; r <= endRow; r++) {
      for (let c = startCol; c <= endCol; c++) {
        backgrounds[r][c] = base;
      }      
    }
  });
  const checkColorObj = new Map(sheet.getRange("J2:J5").getBackgrounds().map(([j]) => [j, 0]));
  backgrounds.forEach(r => r.forEach(c => {
    if (checkColorObj.has(c)) {
      checkColorObj.set(c, checkColorObj.get(c) + 1);
    }
  }));
  const values = [...checkColorObj.values()].map(e => [e]);

  console.log(values); // Result value can be seen in the log.
  sheet.getRange("L2:L5").setValues(values); // Sample script for displaying result value in "L2:L5".
}

Another approach involves implementing this script as a custom function, which would look like the following. To test this script, insert the custom function =SAMPLE("B2:H30") into a cell. Ensure the input is enclosed in quotes, such as "B2:H30", to avoid errors like Exception: Range not found.

function SAMPLE(a1Notation) {
  const sheet = SpreadsheetApp.getActiveSheet();
  const range = sheet.getRange(a1Notation);
  const offsetR = range.getRow() - 1;
  const offsetC = range.getColumn() - 1;
  const backgrounds = range.getBackgrounds();
  range.getMergedRanges().forEach(r => {
    const startRow = r.getRow() - 1 - offsetR;
    const endRow = startRow + r.getNumRows() - 1;
    const startCol = r.getColumn() - 1 - offsetC;
    const endCol = startCol + r.getNumColumns() - 1;
    const base = backgrounds[startRow][startCol];
    for (let r = startRow; r <= endRow; r++) {
      for (let c = startCol; c <= endCol; c++) {
        backgrounds[r][c] = base;
      }      
    }
  });
  const checkColorObj = new Map(sheet.getRange("J2:J5").getBackgrounds().map(([j]) => [j, 0]));
  backgrounds.forEach(r => r.forEach(c => {
    if (checkColorObj.has(c)) {
      checkColorObj.set(c, checkColorObj.get(c) + 1);
    }
  }));
  return [...checkColorObj.values()].map(e => [e]);
}

Testing:

Upon inserting the above custom function into "L2" of the specified Spreadsheet, the resulting values align with your expected outcomes when viewed. The reference color is drawn from the "J2:J5" cells, so exercise caution in that regard.

https://i.sstatic.net/WyhAr.png

Note:

  • This particular script caters to the provided Spreadsheet. Modification of the Spreadsheet may render the script unusable, thus proceed with caution.

References:

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

"Creating a Hyperlink that Navigates to Specific Content Within a

I have a situation where I am trying to link to sections on the page contained within collapsed fieldsets. My goal is that when a user clicks on this link, the page should scroll down and automatically expand the fieldset to display the content inside. E ...

Is Angular's ngOnChanges failing to detect any changes?

Within one component, I have a dropdown list. Whenever the value of the dropdown changes, I am attempting to detect this change in value in another component. However, I am encountering an unusual issue. Sometimes, changing the dropdown value triggers the ...

Discover the process of connecting a REST controller with AngularJS and Spring

I have a list of file paths stored in an array within the scope variable of an AngularJS Controller. My goal is to properly map these file paths to a Java REST controller for further processing. When I pass it as "/ABC/Scope-Variable," it successfully ma ...

Is it possible to customize the color of icons in react's Material-table?

I'm currently utilizing the material-table library and I'm struggling to individually change the color of each icon. Could you assist me with this? I attempted custom CSS, but it's affecting all icons at once instead of specific ones. Here i ...

Unusual symbols in angular variable found within an HTML document

Currently in my HTML, I have code like this: <li ng-repeat="favorite in favorites track by $index"> <a ng-href="javascript:void(0)" ng-click="changeSVG(favorite)"> <i class="fa fa-sitemap"></i>{{favorite}} </a> </l ...

Struggling with determining the perfect transition speed for the sidemenu display

I'm a newcomer to web development and facing an issue with setting the transition speed for opening and closing this side menu. Despite adding transitions in the CSS and specifying duration in the Javascript, the menu continues to open instantly. I di ...

React-Redux error: Unable to call addItem function

Recently, I started learning about redux by following an E-Commerce site tutorial that uses React and Redux. In the tutorial, there is a CollectionItem Component with a button that triggers an addItem function to add the selected item to the shopping cart. ...

Unexpected display behavior observed with Material UI tooltips

In my project, I am using material-ui with react. My goal is to change the tooltip (each containing an icon) when a specific condition is met. ToolTipWrapper.js import React from "react"; import { Tooltip } from "@material-ui/core"; import { CheckCircle ...

Make sure to correctly assign methods to their respective prototypes to avoid confusion

While working on my Electron app with jQuery, I keep encountering an error related to jQuery's tween function. I'm loading jQuery using standard node require: <script type="text/javascript>window.$ = window.jQuery = require('jquery&a ...

Ensuring that the height of this specific div is set to 100% using

Is there a way to utilize the .height() method to fetch the height of each individual .post element and then assign it to the corresponding .left element? I have tried using this() but haven't been able to find a solution... Currently, my jQuery code ...

What is the process for importing a JavaScript export file created from the webpack.config file?

Issue at Hand In the process of testing APIs, I encountered a dilemma in setting up either the DEV or Production environment. This involved configuring API endpoints for local testing and preparing them for production use. To achieve this, I utilized NOD ...

Making API calls using JavaScript

I'm struggling with understanding how to approach this problem in javascript. Here is the question along with the details. I would appreciate any assistance. QUERY We have a server backend that provides two endpoints: /GetLocalPressReleases and /Get ...

Utilizing Jquery tabs for consistent height display

Currently, I am utilizing jquery tabs for showcasing various content. This is what my functions look like: $(function() { $( "#tabs" ).tabs(); }); I attempted to ensure all tabs have the same height by using this approach: var heightStyle = ...

Leveraging the outcome of a Promise within webpack's configuration file

Currently, I am facing a challenge in accessing a Promise which is essentially an array of file paths created within recursive.js. This script navigates through my directories and organizes my stylesheets. The main issue arises when trying to utilize this ...

populate vueJS table with data

I encountered an issue while trying to load data from the database into my table created in VueJS. I have set up my component table and my script in app.js, but I am seeing the following error in the view: [Vue warn]: Property or method "datosUsuario" ...

What is causing the PUT request to not go through when using POSTMAN?

As I navigate through the paths of my application, I encountered an issue with PUT requests that were not being fully processed by POSTMAN. Below is the configuration of my ExpressJS server: const express = require('express'); const morgan = re ...

Troubleshooting: Why Isn't Calling a PHP Function from AJAX Working

I'm attempting to utilize AJAX to call a PHP function. Here's the script I've implemented. <script type="text/javascript" src="jquery.1.4.2.js"> $(document).ready(function () { // after EDIT according to ...

Mysterious data organization - transform into an object

As I interact with an API, there is a value that I cannot quite pinpoint. My goal is to transform this string into a JavaScript object, but the process seems complex and confusing. Does anyone have insight on what this data represents and how it can be co ...

What is the best way to reference an Angular constant within a Gulp configuration file?

Is it possible to retrieve an Angular constant within a Gulp file? For example: angular.module('app').constant('env', { url: 'http://localhost:1337/' }); What is the method for accessing this constant inside a function ...

Angular has trouble displaying information across multiple HTML files

I have created an HTML file to display some data. The initial HTML file named disc-log.html looks like this: <div> <h2>Discs</h2> <jhi-alert></jhi-alert> <div class="container-fluid"> <div class=" ...