Search code examples
google-sheetsconditional-formatting

How to generate very sharp color scale for below zero and above zero?


I'm encountering a big problem when using the number 0 (zero) as a factor for the colors to generate scales, the numbers close to 0 (zero) end up becoming almost white, impossible to see a difference.

The idea is that above 0 (zero) it starts green and gets even stronger and below 0 (zero) starting with a red one and getting stronger.

I really need any number, even if it's 0.000001 already has a visible green and the -0.000001 has a visible red.

Link to SpreadSheet:
https://docs.google.com/spreadsheets/d/1uN5rDEeR10m3EFw29vM_nVXGMqhLcNilYrFOQfcC97s/edit?usp=sharing

Note to help with image translation and visualization:

Número = Number  
Nenhum = None  
Valor Máx. = Max Value
Valor Min. = Min Value

Current Result / Expected Result

enter image description here


Solution

  • After reading your new comments I understand that these are the requisites:

    • The values above zero should be green (with increased intensity the further beyond zero).
    • The values below zero should be red (with increased intensity the further beyond zero).
    • Values near zero should be coloured (not almost white).

    Given those requisites, I developed an Apps Script project that would be useful in your scenario. This is the full project:

    function onOpen() {
      var ui = SpreadsheetApp.getUi();
    
      ui.createMenu("Extra").addItem("Generate gradient", "parseData").addToUi();
    }
    
    function parseData() {
      var darkestGreen = "#009000";
      var lighestGreen = "#B8F4B8";
      var darkestRed = "#893F45";
      var lighestRed = "#FEBFC4";
      var range = SpreadsheetApp.getActiveRange();
      var data = range.getValues();
      var biggestPositive = Math.max.apply(null, data);
      var biggestNegative = Math.min.apply(null, data);
      var greenPalette = colourPalette(darkestGreen, lighestGreen, biggestPositive);
      var redPalette = colourPalette(darkestRed, lighestRed, Math.abs(
        biggestNegative) + 1);
      var fullPalette = [];
    
      for (var i = 0; i < data.length; i++) {
        if (data[i] > 0) {
          var cellColour = [];
    
          cellColour[0] = greenPalette[data[i] - 1];
          fullPalette.push(cellColour);
    
        } else if (data[i] < 0) {
          var cellColour = [];
    
          cellColour[0] = redPalette[Math.abs(data[i]) - 1];
          fullPalette.push(cellColour);
    
        } else if (data[i] == 0) {
          var cellColour = [];
    
          cellColour[0] = null;
          fullPalette.push(cellColour);
        }
      }
      range.setBackgrounds(fullPalette);
    }
    
    function colourPalette(darkestColour, lightestColour, colourSteps) {
      var firstColour = hexToRGB(darkestColour);
      var lastColour = hexToRGB(lightestColour);
      var blending = 0.0;
      var gradientColours = [];
    
      for (i = 0; i < colourSteps; i++) {
        var colour = [];
    
        blending += (1.0 / colourSteps);
        colour[0] = firstColour[0] * blending + (1 - blending) * lastColour[0];
        colour[1] = firstColour[1] * blending + (1 - blending) * lastColour[1];
        colour[2] = firstColour[2] * blending + (1 - blending) * lastColour[2];
        gradientColours.push(rgbToHex(colour));
      }
      return gradientColours;
    }
    
    function hexToRGB(hex) {
      var colour = [];
    
      colour[0] = parseInt((removeNumeralSymbol(hex)).substring(0, 2), 16);
      colour[1] = parseInt((removeNumeralSymbol(hex)).substring(2, 4), 16);
      colour[2] = parseInt((removeNumeralSymbol(hex)).substring(4, 6), 16);
    
      return colour;
    }
    
    function removeNumeralSymbol(hex) {
      return (hex.charAt(0) == '#') ? hex.substring(1, 7) : hex
    }
    
    function rgbToHex(rgb) {
      return "#" + hex(rgb[0]) + hex(rgb[1]) + hex(rgb[2]);
    }
    
    function hex(c) {
      var pool = "0123456789abcdef";
      var integer = parseInt(c);
    
      if (integer == 0 || isNaN(c)) {
        return "00";
      }
    
      integer = Math.round(Math.min(Math.max(0, integer), 255));
    
      return pool.charAt((integer - integer % 16) / 16) + pool.charAt(integer % 16);
    }
    

    First of all the script will use the Ui class to show a customised menu called Extra. That menu calls the main function parseData, that reads the whole selection data with getValues. That function holds the darkest/lightest green/red colours. I used some colours for my example, but I advise you to edit them as you wish. Based on those colours, the function colourPalette will use graphical linear interpolation between the two colours (lightest and darkest). That interpolation will return an array with colours from darkest to lightest, with as many in-betweens as the maximum integer in the column. Please notice how the function uses many minimal functions to run repetitive tasks (converting from hexadecimal to RGB, formatting, etc…). When the palette is ready, the main function will create an array with all the used colours (meaning that it will skip unused colours, to give sharp contrast between big and small numbers). Finally, it will apply the palette using the setBackgrounds method. Here you can see some sample results: Example

    In that picture you can see one set of colours per column. Varying between random small and big numbers, numerical series and mixed small/big numbers. Please feel free to ask any doubt about this approach.