I am supporting CandidateX

CandidateX is a startup that focuses on creating inclusion-focused hiring solutions, designed to increase access to job opportunities for underestimated talent. Check them out if you have a few minutes to spare. They need visibility!

Update Nov2017
UPDATE:  parse.com as now been closed and moved to parseplatform.org. I will update the content of this page an move the back-end to this platform

This is an interesting d3.js and parse.com mashup of a number of topics covered on this site.

The target is to build an app that

Example

Here’s a snap of the thing. Given an input color the parameters here will
  • Use the Pantone Fashion Home color scheme
  • Use the LABch color model
  • Create palettes for hue, saturation and lightness, consisting only of members of chosen scheme
  • Base the palettes on the 4 nearest in scheme colors to the target color
  • Pop up color parameter data on hovering over a color
  • Clicking on any color in the chart will cause it to be redrawn using the clicked on color as target.
  • Changing scheme will show the same chart, but based on the newly selected scheme

d3

Live example here

The Code

<!DOCTYPE HTML>
<html>
    
    <head>
        <title>A mashup of parse.com and d3.js</title>
        <link rel="stylesheet" type="text/css" href="http://xliberation.com/parse/colortable/css/schemer.css">
        <link rel="stylesheet" type="text/css" href="//cdn.jsdelivr.net/tipsy/1.0/stylesheets/tipsy.css">
        <style>
            .tiptable TD
                {
                    color:white;
                    background-color:chocolate;
                }
        </style>
        <script type="text/javascript" src="//www.google.com/jsapi"></script>
        <script type="text/javascript" src="//d3js.org/d3.v3.min.js"></script>
        <script src="//ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.min.js"></script>
        <script src="//cdn.jsdelivr.net/tipsy/1.0/javascripts/jquery.tipsy.js"></script>
        <script src="//www.parsecdn.com/js/parse-1.2.2.min.js"></script>
        <script src="//xliberation.com/parse/colortable/js/colortable.js"></script>
        <script src="//xliberation.com/parse/colortable/js/parsch.js"></script>

        <script type="text/javascript">
            // globals
            var schemeFromParse;
        </script>
        <script type="text/javascript">
            google.load('visualization', '1');
        </script>
        <script type="text/javascript">

        (function(){
            google.setOnLoadCallback(function () {

                // start with a random color
                $('#hexinput').val('#'+Math.floor(Math.random()*16777215).toString(16));
                
                // the options
                addSelectOptions($('#schemeselector'),getKnownSchemes());
                addSelectOptions($('#modelselector'),getModels());
                addSelectOptions($('#maxselector'),[1,2,3,4,5,6,7,8,9,10],4);
                addSelectOptions($('#swatchselector'),[1,2,3,4,5,6,7,8,9,10],5);
                addSelectOptions($('#textselector'),["yes","no"],"yes");
                
                addListeners();
                
                // get the useful code from google apps script
                $('#status').text("...getting google apps script modules");
                makeLight("gas","busy");
                $.when.apply($,loadModules (["vEquivalents","hacks","usefulColors"],"mcpher")).then (
                    function () {
                        makeLight("gas","idle");
                        makeLight("parse","busy");
                        getSelectedScheme(getChosenScheme().split(":")[0])
                            .done( function() {
                                makeLight("parse","idle");
                                changeDetected();
                            });
                        
                    },
                    function (error) {
                        console.log(JSON.stringify(error));
                    });
            });
            
            function getUiOptions (label) {
                return {
                    swatchSize : getChosenSwatchSize(),
                    maxNearest : getChosenMaxNearest(),
                    scheme : getChosenScheme(),
                    model : getChosenModel(),
                    target: getChosenColor(),
                    label : label ? label : getChosenColor()
                };
            }

            function fillBar() {
                // create random palette - different colored headers each time
                var p = makeColorProps(htmlHexToRgb(getChosenColor()));
                $('.paint')
                    .css("background-color",p.htmlHex)
                    .css("color",rgbToHex(p.textColor));
                return p;
            }

            function constructData (options) {
                // build data for sunburst chart
                var promise = $.Deferred();
                schemeFromParse.done ( function (schemeArray) {
                    // make all the data
                    makeLight("color","busy");
                    var data = makeAllPalettes(options,schemeArray);
                    promise.resolve(data);
                    makeLight("color","idle");
                });
                return promise.promise();
            }
           
            
            function makeAllPalettes(options,schemeArray) {
                var props = ["hue", "lightness", "saturation"];
                var pBase = makeColorProps(htmlHexToRgb(options.target));
                var base = 
                    { name : "base" + getConfigKey() , 
                        color: pBase, 
                        options:options, 
                        label:options.label,
                        children :[],
                        size:0
                    };

                // take a sorted copy of the schemearray
                var nearest = sortClosestColors(pBase.rgb,schemeArray.slice(0)); 
                // need to repeat nearest object for each prop
                for (var j=0;j<arrayLength(props);j++) {
                    // name type of palette
                    var cBase = { 
                        name : "na"+"pa"+j+"sa" + getConfigKey(),
                        color: pBase,
                        prop: props[j],
                        label:props[j],
                        children :[],
                        size: 0
                    };
                    base.children.push(cBase);

                    for (var k=0;k<options.maxNearest;k++) {
                        var p = { 
                            name : "n" + k + "p" + j + "sb" + getConfigKey(),
                            color: nearest[k].colorProps,
                            prop: props[j],
                            label:nearest[k].label,
                            children :[],
                            size: compareColorProps(nearest[k].colorProps,pBase)
                        }
                        cBase.children.push(p);
                        // now add a palette
                        var palette = makeAPalette(p.color.rgb, options.model,
                                                props[j], options.swatchSize, props[j] == "lightness");
                        // find the nearest match to each item in the palette
                        // each becomes a child to the previous palette member
                        var t = p.children;
                        for (var m =0; m < options.swatchSize ; m++) {
                            var near = sortClosestColors(palette[m].rgb,schemeArray)[0];
                            t.push ({ 
                                name :  "n"+k+"p"+j+"s"+m + getConfigKey(),
                                color: near.colorProps,
                                prop: props[j],
                                label: near.label,
                                children :[],
                                size: compareColorProps(near.colorProps,palette[m])
                            });
                            t = t[0].children;
                        }
                    }
                }
                return base;
                
            }

            function addListeners () {
                $(".detect").click(function(){
                    changeDetected();
                });
                
                $('#schemeselector').change(function() {
                    makeLight("parse","busy");
                    getSelectedScheme($(this).val().split(":")[0])
                        .done( function() {
                            makeLight("parse","idle");
                            changeDetected();
                        });
                });
                                   
            }

           // need this to be accessible globally    
            var mGlob = {
               height: 600,
               padding: 5,
               duration: 1200, 
               counter:0,
               configKey: ""
            };

            function changeDetected() {

                // get the new data
                fillBar();
                makeLight("d3","busy");
                mGlob.width = Math.min(mGlob.height,  $('#d3container').width());
                mGlob.radius = mGlob.width/2;
                var constructPromise = constructData (getUiOptions(getChosenColor()));
                                                                    
                var svg = mGlob.svg || d3.select("#d3container").append("svg:svg")
                    .attr("width", mGlob.width)
                    .attr("height", mGlob.height)
                    .append("g")
                    .attr("transform", "translate(" + mGlob.width / 2 + "," + mGlob.height /2 + ")");                
                mGlob.svg  = svg;
                
                var partition =  d3.layout.partition()
                    .sort(null)
                    .size([2 * Math.PI, mGlob.radius])
                    .value(function(d) { return 1; });
                
                var arc = d3.svg.arc()
                    .startAngle(function(d) { return d.x; })
                    .endAngle(function(d) { return d.x + d.dx; })
                    .innerRadius(function(d) { return d.y; })
                    .outerRadius(function(d) { return d.y + d.dy; });
            
                // transition to it
                constructPromise.done( function(data) {

                    var nodes = partition.nodes(data);
                    var pathData = svg.selectAll("path").data(nodes,function(d){return d.name;});

                    var pathEnter = pathData.enter();
                    var path = pathEnter
                      .append("svg:path")
                        .attr("d", arc)
                        .style("stroke", "#fff")
                        .on("click", click);
                    
                        
                    // start white for rescales
                    if(mGlob.configKey != getConfigKey()) { 
                        path.style("fill", "#ffffff");
                        mGlob.configKey = getConfigKey();
                    }
                    

                    // add new
                    pathData.transition()
                        .duration(mGlob.counter ? mGlob.duration : 0)
                        .style("fill", function(d) { return  d.color.htmlHex; });
               
                                   // fade out old
                    pathData.exit().transition()
                       .duration(0)
                       .style("fill", function(d) { return "#ffffff"});
                    
                    
                    // add fancy tool tips
                    $('path').tipsy({ 
                        gravity: $.fn.tipsy.autoWE, 
                        html: true, 
                        delayIn: 1000,
                        title: function() {
                          return  makeTipsy(this.__data__); }
                      });
                    
                    mGlob.counter++;
                    makeLight("d3","idle");
                });

                function makeTipsy(d) {
                    var c = d.color;
                    return "<div style='background-color:" + 
                        c.htmlHex + ";color:" + 
                        rgbToHex(c.textColor) + ";'>" + 
                        d.label + "(" + getChosenScheme() + ")" + "</div>" +
                        "<table class='tiptable'><tbody>" + 
                        makeATableRow ("rgb",c.htmlHex,c.red,c.green,c.blue) +
                        makeATableRow ("lab",c.LStar.toFixed(1),c.aStar.toFixed(1),c.bStar.toFixed(1)) +
                        makeATableRow ("lch",c.LStar.toFixed(1),c.cStar.toFixed(1),c.hStar.toFixed(1)) +
                        makeATableRow ("xyz",c.x.toFixed(1),c.y.toFixed(1),c.z.toFixed(1)) +
                        makeATableRow ("hsl-v",c.hue.toFixed(1),c.saturation.toFixed(1),c.lightness.toFixed(1),c.value.toFixed(1)) +
                        makeATableRow ("cmyk",c.cyan.toFixed(2),c.magenta.toFixed(2),c.yellow.toFixed(2),c.black.toFixed(2)) +
                        makeATableRow ("contrast",c.contrastRatio.toFixed(1),"closeness",d.size.toFixed(2)) +
                        makeATableRow ("model",getChosenModel(),"palette",d.prop) +
                        "</tbody></table>";
                    
                }
                function makeATableRow() {
                    var t= "<tr>";    
                
                    for (var i = 0; i < arguments.length; i++) {
                        t += "<td>" + arguments[i] + "</td>";
                    }
                    return t + "</tr>";
                    
                }
                function click(d) {
                    $('#hexinput').val(d.color.htmlHex);
                    changeDetected();
                }

            }
            function makeLight (what,img) {
                 $('#'+what+'busy').attr('src', "//xliberation.com/cdn/img/" +img + ".gif"); 
            }
            function getChosenColor() {
                return $('#hexinput').val();
            }
            function getChosenScheme() {
                return $('#schemeselector').val();
            }
            function getChosenSwatchSize() {
               return $('#swatchselector').val(); 
            }
            function getChosenMaxNearest() {
                return $('#maxselector').val();
            }
            function getChosenModel() {
                return $('#modelselector').val();
            }
            function getChosenTextNeeded() {
                return $('#textselector').val()=="yes";
            }
            function getConfigKey() {
                // we use this to force a d3 data refresh if dimensions change 
                return "s" + getChosenSwatchSize() + "m" + getChosenMaxNearest()  ;
            }
        }());
        </script>
    </head>
    
    <body>
        <div id="randombar" class="paint">
            <div><h3>d3 sunburst chart from color schemes held in parse.com</h3>
                <div>Sunburst design based on 
                    <a href='http://www.cc.gatech.edu/gvu/ii/sunburst/'>John Stasko.</a>
                    d3.js by <a href='http://bl.ocks.org/mbostock/4063423'>Mike Bostok.</a> 
                    Mashup by <a href='https://ramblings.mcpher.com/color-fiesta/find-nearest-color-match/'> 
                    Bruce McPherson</a>
                </div>
            </div>
        </div>
        <div>

            <div class="label">
                <div class = "button detect" id ="go">Apply this hex color or click on a color in the circle</div>
                <input type="text"" class = "text" id ="hexinput"></input>
                            <div class="text">
                
                gas<img id="gasbusy" class="busy" src="//xliberation.com/cdn/img/idle.gif"></img>
                parse<img id="parsebusy" class="busy" src="//xliberation.com/cdn/img/idle.gif"></img>
                color<img id="colorbusy" class="busy" src="//xliberation.com/cdn/img/idle.gif"></img>
                d3<img id="d3busy" class="busy" src="//xliberation.com/cdn/img/idle.gif"></img>
            </div>
                <br>
            </div>
            <div class="label">Scheme <select name="schemeselector" id="schemeselector"></select></div>
            <div class="label detect">Model <select name="modelselector" id="modelselector"></select></div>
            <div class="label detect">Swatch size <select name="swatchselector" id="swatchselector"></select></div>
            <div class="label detect">Maximum nearest <select name="maxselector" id="maxselector"></select></div>
            <div class="text" id="status"></div>

            <div id="d3container" style-"width:80%"></div>
        </div>
        
        <div id="show" style="width:100%;"></div>
        
    </body>

</html>

Library

For more about parse.com, see Parse.com

    function loadModules(modules, sourceLibrary) {
        var promises = [];
        // now does this in one call - returns promise array for backward compat.
        var url = "https://script.google.com/macros/s/AKfycbzhzIDmgY9BNeBu87puxMVUlMkJ4UkD_Yvjdt5MhOxR1R6RG88/exec";
        var u = url + "?source=script&type=javascript&module=" + modules.join() + "&library=" + sourceLibrary;
        promises.push ($.getScript(u)); 
        return promises;
    }
    
        // we can do this asynch as soon as it's known
    function getSelectedScheme (scheme) {
        //get current parse data for scheme - async
        $('#status').text("...getting selected scheme " + scheme);
        var parseKeys = getParseKeys();
        Parse.initialize(parseKeys.appId, parseKeys.jsKey);
        var ColorTable = Parse.Object.extend("ColorTable");
        schemeFromParse = schemePromise(ColorTable, scheme);
        schemeFromParse.fail(function (error) {
            $('#status').text("error getting scheme " + scheme + JSON.stringify(error));
        });
        schemeFromParse.done(function (results) {
            $('#status').text("found " + results.length + " colors in scheme " + scheme );
        });
        return schemeFromParse;
    }
    
    // return in chunks to the parse.com limit
    function findChunk(model, scheme, allData) {
        // we have to find in chunks since there is a limit on query size
        // will return a promise
        var limit = 1000;
        var skip = allData.length;
        var findPromise = $.Deferred();
        var query = new Parse.Query(model);
        if (scheme) query.equalTo("scheme", scheme);
        query
            .limit(limit)
            .skip(skip)
            .find()
            .then(function (results) {
            findPromise.resolve(allData.concat(results), !results.length);
        }, function (error) {
            findPromise.reject(error);
        });
        return findPromise.promise();
    }
    
    // get the data for chosen scheme from parse
    function schemePromise(model, scheme, allResults, allPromise) {
        // find a scheme at a time
        var schemeArray = [];
        var promise = allPromise || $.Deferred();

        findChunk(model, scheme, allResults || [])
            .done(function (results, allOver) {
            if (allOver) {
                // we are done
                // normalize to an array from parse ob
                for (var i =0;i<results.length;i++) {
                    var hex= results[i].get("hex");
                    // we'll calculate color props since we'll need them later anyway. so its just one time
                    try { 
                        var x = parseInt(Right(hex, Len(hex) - 1),16);
                        schemeArray.push({label: results[i].get("label"), 
                                        hex: hex,
                                        scheme: results[i].get("scheme"),
                                        code: results[i].get("code"),
                                        key: results[i].get("key"),
                                        colorProps: makeColorProps(htmlHexToRgb(hex)) });
                    }
                    catch(err) {
                        console.log(err + " converting scheme color hex " + hex);
                    }
                }
                promise.resolve(schemeArray);
            } else {
                // may be more
                schemePromise(model, scheme, results, promise);
            }
        })
        .fail(function (error) {
            promise.reject(error);
        });
        return promise.promise();
    }
    
            
    function sortClosestColors (targetRgb, schemeArray) {
      var targetProps = makeColorProps (targetRgb);
      for (var i= 0; i < schemeArray.length ; i++) {
        schemeArray[i].comparison = compareColorProps (targetProps, schemeArray[i].colorProps);
      }
      return schemeArray.sort (comparisonSort);
    }
    
    function comparisonSort(a,b) {
      if (a.comparison < b.comparison)
         return -1;
      if (a.comparison > b.comparison)
        return 1;
      return 0;
    }
    
    function addSelectOptions(selectElem,selectValues,defaultValue) {
        for (var i = 0; i < selectValues.length; i++ ) {
            selectElem
                .append($("<option/>", {
                    value: selectValues[i],
                    text: selectValues[i]
                }));    
        }
        if (!(typeof defaultValue == 'undefined'))selectElem.val(defaultValue);
    }
    function getKnownSchemes() {
        // there isnt a unique values type of structure, so we'll need to do something better later
        return  ["pfh:Pantone fashion home","pms:Pantone match system",
                    "dulux:Dulux paints","htm:Html Colors","raf:airplane colors"];
    }
    
    function getModels() {
        return ["lch","hsl"];
    }

GAS code

The color manipulation modules are hosted on and served up by Google Apps Script. For more about color functions see Playing around with GAS color
//this is all about colors
var VBCOLORS = Object.freeze(
{vbBlue: 16711680,  
 vbWhite: 16777215,
 vbBlack: 0,
 vbGreen: 65280,
 vbYellow: 65535,
 vbRed:255
 }
);
var ECOMPARECOLOR = Object.freeze (
{
  whiteX  : 95.047,
  whiteY : 100,
  whiteZ  : 108.883,
  eccieDe2000 : 21000,
  beige: 10009301
}
);
function speedTest() {
  useTimer('x','10000 iterations of color comparison').start();
  for (var i = 1 ; i <= 10000 ;i++) {
    compareColors (i, VBCOLORS.vbWhite - i);
  }
  useTimer('x').stop();
  return useTimer().report();
}  
function testCompareSpeed() {
    Logger.log(speedTest());
}

function beigeness(rgbColor) {
  return compareColors (rgbColor, ECOMPARECOLOR.beige);
}
function RGB(r,g,b) {
  return Math.round(r) + (Math.round(g) << 8) + (Math.round(b) << 16) ;
}
function rgbRed(rgbColor) {
    return  rgbColor % 0x100;
}
function rgbGreen(rgbColor) {
    return Math.floor(rgbColor / 0x100) % 0x100;
}
function rgbBlue(rgbColor) {
    return Math.floor(rgbColor / 0x10000) % 0x100;
}
function heatmapColor(min , max, value) {
  return rampLibraryRGB("heatmap", min, max, value);
}
function rgbToHTMLHex(rgbColor) {
    // just swap the colors round for rgb to bgr as bit representation is reversed
    return "#" + maskFormat(RGB(rgbBlue(rgbColor), 
            rgbGreen(rgbColor), rgbRed(rgbColor)).toString(16), "000000");
}
function rgbToHex(rgbColor) {
  // this is just a convenience pass throuh .. i originally screwed up the camel case 
  return rgbToHTMLHex(rgbColor);
}
function rampLibraryHex (sName, min , max, value , optBrighten) {
  return rgbToHTMLHex(rampLibraryRGB (sName, min , max, value , optBrighten));
}

function maskFormat(sIn , f ) {
    var s = Trim(sIn);
    if (Len(s) < Len(f)) {
        s = Left(f, Len(f) - Len(s)) + s ;
    }
    return s;
}
function lumRGB(rgbCom, brighten) {
    var x = rgbCom * brighten;
    return x > 255 ?  255 : 
                      x < 0 ? 0 : x;
}


function contrastRatio(rgbColorA, rgbColorB) {
    var lumA = w3Luminance(rgbColorA);
    var lumB = w3Luminance(rgbColorB);
    return (Math.max(lumA, lumB) + 0.05) / (Math.min(lumA, lumB) + 0.05);
}

function makeColorProps (rgbColor) {
  return new colorProps().populate(rgbColor);
}

function colorProps() {
  return this;
}
colorProps.prototype.populate = function (rgbColor) {
  var p = this;

    //store the source color
    p.rgb = rgbColor;
    
    //split the components
    p.red = rgbRed(rgbColor);
    p.green = rgbGreen(rgbColor);
    p.blue = rgbBlue(rgbColor);
    
    //the html hex rgb equivalent
    p.htmlHex = rgbToHTMLHex(rgbColor);
    
    //the w3 algo for luminance
    p.luminance = w3Luminance(rgbColor);
    
    //determine whether black or white background
    if (p.luminance < 0.5) 
        p.textColor = VBCOLORS.vbWhite;
    else
        p.textColor = VBCOLORS.vbBlack;
   

    //contrast ratio - to comply with w3 recs 1.4 should be at least 10:1 for text
    p.contrastRatio = contrastRatio(p.textColor, p.rgb);
    
    //cmyk - just an estimate
    p.black = Math.min(Math.min(1 - p.red / 255, 1 - p.green / 255), 1 - p.blue / 255);
    if (p.black < 1) {
        p.cyan = (1 - p.red / 255 - p.black) / (1 - p.black);
        p.magenta = (1 - p.green / 255 - p.black) / (1 - p.black);
        p.yellow = (1 - p.blue / 255 - p.black) / (1 - p.black);
    }

    // calculate hsl + hsv and other wierd things
    var p2 = rgbToHsl(p.rgb);
    p.hue = p2.hue;
    p.saturation = p2.saturation;
    p.lightness = p2.lightness;
    
    p.value = rgbToHsv(p.rgb).value;
    
    p2 = rgbToXyz(p.rgb);
    p.x = p2.x;
    p.y = p2.y;
    p.z = p2.z;
    
    p2 = rgbToLab(p.rgb);
    p.LStar = p2.LStar;
    p.aStar = p2.aStar;
    p.bStar = p2.bStar;
    
    p2 = rgbToLch(p.rgb);
    p.cStar = p2.cStar;
    p.hStar = p2.hStar;

  return p;

}


function w3Luminance (rgbColor) {
// this is based on
// http://en.wikipedia.org/wiki/Luma_(video)

  return (0.2126 * Math.pow((rgbRed(rgbColor)/255),2.2)) +
         (0.7152 * Math.pow((rgbGreen(rgbColor)/255),2.2)) +
         (0.0722 * Math.pow((rgbBlue(rgbColor)/255),2.2)) ;
}


function cellProperty (r,p) {
  // makes it compatible with VBA version
  switch(p) {
    case  "background-color":
      return sheetCache(r,"getBackgroundColors").getValues(r);

    case "color":
      return sheetCache(r,"getFontColor").getValues(r);
   
    case "font-size":
      return sheetCache(r,"getFontSize").getValues(r);

    default:
      DebugAssert (false, "unknown cellproperty request " + p);
  }
}

function cellCss(r, p ) {
    return  p + ":" + cellProperty(r, p) + ";" ;
}

function rgbExpose(r , g , b ) {
    // redundadnt .. just for compatibility with vba
    return RGB(r, g, b) ;
}

function htmlHexToRgb(htmlHex){

    var s = LTrim(RTrim(htmlHex));
    s =  (Left(s, 1) == "#" ? '' : '#') + s;
    DebugAssert (Len(s) > 1 && Left(s, 1) == "#", "invalid hex color" + htmlHex);
    // -- need to find equivalent ---
    var x = parseInt(Right(s, Len(s) - 1),16);
    // these are purposefully reversed since byte order is different in unix
    return RGB(rgbBlue(x), rgbGreen(x), rgbRed(x));
}
function hslToRgb(p) {
    // from // http://www.easyrgb.com/
    var x1 , x2 , h, s , l , 
        red , green , blue ;
      
    h = p.hue / 360;
    s = p.saturation / 100;
    l = p.lightness / 100;
    
    if (s == 0) {
        return RGB (l * 255, l * 255, l * 255);
    }
    else {
        if (l < 0.5 )
            x2 = l * (1 + s);
        else
            x2 = (l + s) - (l * s);
       
        x1 = 2 * l - x2;
        
        red = 255 * hueToRgb(x1, x2, h + (1 / 3));
        green = 255 * hueToRgb(x1, x2, h);
        blue = 255 * hueToRgb(x1, x2, h - (1 / 3));
        return RGB (red, green, blue);
     }
}
function hueToRgb(a , b , h ) {
   // from // http://www.easyrgb.com/
    if (h < 0)  h = h + 1;
    if (h > 1)  h = h - 1;
    DebugAssert (h >= 0 && h <= 1,"hue outside range 0-1:" + h);

    if (6 * h < 1) 
        return a + (b - a) * 6 * h;
    else {
      if (2 * h < 1) 
          return b;
      else {
        if (3 * h < 2) 
          return a + (b - a) * ((2 / 3) - h) * 6;
        else
          return a;
      }
    }   
}


function hexColorOf(r) {
    return cellProperty(r,"color") ;
}
function colorizeCell(target, c ) {
    
    if (Len(c) > 1 && Left(c, 1) == "#") {
        p = makeColorProps(htmlHexToRgb(c));
        //target.Interior.Color = p.rgb
        //target.Font.Color = p.textColor
    }
}
function rampLibraryRGB(ramp, min , max, value , optBrighten) {
  var brighten = fixOptional (optBrighten, 1);
  
  if (IsArray(ramp)) {
    // ramp colors have been passed here
       return colorRamp(min, max, value, ramp,undefined , brighten);
  }
  else {
  
    switch(Trim(LCase(ramp))) {
        case "heatmaptowhite":
            return  colorRamp(min, max, value, 
                            [VBCOLORS.vbBlue, VBCOLORS.vbGreen, VBCOLORS.vbYellow, 
                             VBCOLORS.vbRed, VBCOLORS.vbWhite],undefined , 
                            brighten);
        case "heatmap":
            return  colorRamp(min, max, value, 
                            [VBCOLORS.vbBlue, VBCOLORS.vbGreen, VBCOLORS.vbYellow, 
                             VBCOLORS.vbRed], undefined, 
                            brighten);  
        case "blacktowhite":
            return  colorRamp(min, max, value, 
                            [VBCOLORS.vbBlack, VBCOLORS.vbWhite], undefined, 
                            brighten);    
        case "whitetoblack":
            return  colorRamp(min, max, value, 
                            [VBCOLORS.vbWhite, VBCOLORS.vbBlack],undefined , 
                            brighten);      
        case "hotinthemiddle":
            return  colorRamp(min, max, value, 
                            [VBCOLORS.vbBlue, VBCOLORS.vbGreen, 
                             VBCOLORS.vbYellow,VBCOLORS.vbRed,VBCOLORS.vbGreen,
                             VBCOLORS.vbBlue], undefined, 
                             brighten);  
        case "candylime":
            return  colorRamp(min, max, value, 
                            [RGB(255, 77, 121), RGB(255, 121, 77), 
                                    RGB(255, 210, 77), RGB(210, 255, 77)], undefined, 
                            brighten);  
                            
        case "heatcolorblind":
            return  colorRamp(min, max, value, 
                            [VBCOLORS.vbBlack,VBCOLORS.vbBlue,  VBCOLORS.vbRed, 
                             VBCOLORS.vbWhite], undefined, 
                            brighten);  
        case "gethotquick":
            return  colorRamp(min, max, value, 
                            [VBCOLORS.vbBlue,VBCOLORS.vbGreen,  VBCOLORS.vbYellow, 
                             VBCOLORS.vbRed],[0, 0.1, 0.25, 1] , 
                            brighten);                           
        case "greensweep":
            return  colorRamp(min, max, value, 
                            [RGB(153, 204, 51), RGB(51, 204, 179)] 
                             ,undefined , 
                            brighten);   
        case "terrain":
            return  colorRamp(min, max, value, 
                            [VBCOLORS.vbBlack, RGB(0, 46, 184), RGB(0, 138, 184), 
                            RGB(0, 184, 138), 
                            RGB(138, 184, 0), RGB(184, 138, 0), 
                            RGB(138, 0, 184), VBCOLORS.vbWhite] 
                             ,undefined , 
                            brighten); 
        case "terrainnosea":
            return  colorRamp(min, max, value, 
                            [VBCOLORS.vbGreen, RGB(0, 184, 138), 
                            RGB(138, 184, 0), RGB(184, 138, 0), 
                            RGB(138, 0, 184), VBCOLORS.vbWhite] 
                             , undefined, 
                            brighten); 
                            
                            
                            
        case "greendollar":
           return colorRamp(min, max, value, 
                                [RGB(225, 255, 235), 
                                RGB(2, 202, 69)], undefined, 
                                brighten);
    
        case "lightblue":
          return colorRamp(min, max, value, 
                                [RGB(230, 237, 246), 
                                RGB(163, 189, 271)], undefined, 
                                brighten);
                                
         case "lightorange":
           return colorRamp(min, max, value, 
                                [rgb(253, 233, 217), 
                                rgb(244, 132, 40)], undefined, 
                                brighten);
                            
       default:
         DebugAssert(false,"Unknown library entry " + ramp) ;
    }
   }
   
}
function colorRamp(min, max , value , mileStones , fractionStones, optBrighten) {
  // color ramp given or default
  var brighten = fixOptional ( optBrighten,1);
  var ms = IsMissing(mileStones) ? 
      [VBCOLORS.vbBlue,VBCOLORS.vbGreen,VBCOLORS.vbYellow, 
        VBCOLORS.vbRed,VBCOLORS.vbRed,VBCOLORS.vbWhite] :
        mileStones;
   DebugAssert( ms.length , "No milestone colors specified");
   // only 1 milestone - thats the color
    if (ms.length == 1) return ms[lb];
   // fractions of range at which to apply these colors
    var fs = [];
    if (!IsMissing(fractionStones)) {
        DebugAssert( fractionStones.length == ms.length, "no of fractions must equal number of steps" );
        fs = fractionStones;
    }
    else {
      // equal proportions
      fs[0]=0;
      for (var i = 1 ; i < ms.length ; i++ ) fs[i] = i/(ms.length-1) ;
    }
    // now calculate the color
    var spread = max - min;
    DebugAssert (spread >= 0 , "min is greater than max for color spread");
    var ratio = (value - min) / spread;
    DebugAssert (ratio >= 0 && ratio <= 1, "couldnt calculate ratio for color spread");
    //find which slot this value belongs in
    for (var i = 1; i  < ms.length;i++) {
      if (ratio <= fs[i]) {
        var r = (ratio - fs[i - 1]) / (fs[i] - fs[i - 1]);
        var red = rgbRed(ms[i - 1]) + (rgbRed(ms[i]) - rgbRed(ms[i - 1])) * r;
        var blue = rgbBlue(ms[i - 1]) + (rgbBlue(ms[i]) - rgbBlue(ms[i - 1])) * r;
        var green = rgbGreen(ms[i - 1]) + (rgbGreen(ms[i]) - rgbGreen(ms[i - 1])) * r;
        return RGB(lumRGB(red, brighten), 
                   lumRGB(green, brighten), 
                   lumRGB(blue, brighten));
      }
    }
        
    DebugAssert (false,"ColorRamp failed to work - dont know why");
}


function rgbToHsl (RGBcolor) {
    //from // http://www.easyrgb.com/
    var r , g , b , d , dr , dg , db , mn , mx , p ={};

    r = rgbRed(RGBcolor) / 255 ;
    g = rgbGreen(RGBcolor) / 255;
    b = rgbBlue(RGBcolor) / 255;
    mn = Math.min(Math.min(r, g), b);
    mx = Math.max(Math.max(r, g), b);
    d = mx - mn;
    
    //HSL sets here
    p.hue = 0;
    p.saturation = 0;
    //lightness
    p.lightness = (mx + mn) / 2;
    
    if (d != 0) {
        // saturation
        if (p.lightness < 0.5)
            p.saturation = d / (mx + mn) ;
        else
            p.saturation = d / (2 - mx - mn) ;       
        // hue
        dr = (((mx - r) / 6) + (d / 2)) / d ;
        dg = (((mx - g) / 6) + (d / 2)) / d ;
        db = (((mx - b) / 6) + (d / 2)) / d ;
        
        if (r == mx) 
            p.hue = db - dg ;
        else 
          if(g == mx) 
            p.hue = (1 / 3) + dr - db ;
          else
            p.hue = (2 / 3) + dg - dr ;
        
        
        //force between 0 and 1
        if (p.hue < 0) p.hue = p.hue + 1 ;
        if (p.hue > 1) p.hue = p.hue - 1 ;
        if (!(p.hue >= 0 && p.hue <= 1)) p.hue = 0;   // " invalid hue " + p.hue + ":" + JSON.stringify(p));
       
    }
    p.hue = p.hue * 360 ;
    p.saturation = p.saturation * 100 ;
    p.lightness = p.lightness * 100 ;
    return p;
    
}

function rgbToHsv(rgbColor){
    // adapted from // http://www.easyrgb.com/
    
    var r = rgbRed(rgbColor) / 255;
    var g = rgbGreen(rgbColor) / 255;
    var b = rgbBlue(rgbColor) / 255;
    var mn = Math.min(r, g, b);
    var mx = Math.max(r, g, b);
    
    // this is the same as hsl and hsv are the same.
    var p = rgbToHsl(rgbColor);
    
    // HSV sets here
    p.value = mx;
    
    return p;
}

function xyzCorrection(v) {
    if (v > 0.04045) 
        return Math.pow( ((v + 0.055) / 1.055) , 2.4);
    else
        return v / 12.92 ;
   
}

function xyzCieCorrection(v) {
  return v > 0.008856 ? Math.pow(v , 1 / 3) : (7.787 * v) + (16 / 116);
}
function rgbToXyz(rgbColor) {
    // adapted from // http://www.easyrgb.com/

    var r = xyzCorrection(rgbRed(rgbColor) / 255) * 100;
    var g = xyzCorrection(rgbGreen(rgbColor) / 255) * 100;
    var b = xyzCorrection(rgbBlue(rgbColor) / 255) * 100;
    var p = new colorProps();
    p.x = r * 0.4124 + g * 0.3576 + b * 0.1805;
    p.y = r * 0.2126 + g * 0.7152 + b * 0.0722;
    p.z = r * 0.0193 + g * 0.1192 + b * 0.9505;

    return p;
}


function rgbToLab(rgbColor) {
    // adapted from // http://www.easyrgb.com/

    var p = rgbToXyz(rgbColor);
    
    var x = xyzCieCorrection(p.x / ECOMPARECOLOR.whiteX);
    var y = xyzCieCorrection(p.y / ECOMPARECOLOR.whiteY);
    var z = xyzCieCorrection(p.z / ECOMPARECOLOR.whiteZ);

    p.LStar = (116 * y) - 16;
    p.aStar = 500 * (x - y);
    p.bStar = 200 * (y - z);

    return p;
}
function compareColorProps (p1, p2 , optCompareType)  {
    
    switch (fixOptional(optCompareType, ECOMPARECOLOR.eccieDe2000)) {
        case ECOMPARECOLOR.eccieDe2000:
            var t= cieDe2000(p1, p2);
            p1 = p2 = null;
            return t;
            
        default:
            DebugAssert (false, "unknown color comparision " + optCompareType);
   }
    
}


function compareColors(rgb1, rgb2 , optCompareType)  {

    return compareColorProps(makeColorProps(rgb1),makeColorProps(rgb2));
    
}

function cieDe2000(p1, p2 ) {
    // calculates the distance between 2 colors using CIEDE200
    // see http://www.ece.rochester.edu/~gsharma/cieDe2000/cieDe2000noteCRNA.pdf
    
    var kp = Math.pow(25 , 7), kl = 1,kc = 1, kh = 1;
    
    // calculate c & g values
    var c1 = Math.sqrt(Math.pow(p1.aStar , 2) + Math.pow(p1.bStar , 2));
    var c2 = Math.sqrt(Math.pow(p2.aStar , 2) + Math.pow(p2.bStar , 2));
    var c = (c1 + c2) / 2;
    var g = 0.5 * (1 - Math.sqrt(Math.pow(c , 7) / (Math.pow(c , 7) + kp)));

    //adjusted ab*
    var a1 = (1 + g) * p1.aStar;
    var a2 = (1 + g) * p2.aStar;

    // adjusted cs
    var c1Tick = Math.sqrt(a1 *a1 + p1.bStar *p1.bStar);
    var c2Tick = Math.sqrt(a2 *a2 + p2.bStar * p2.bStar);

    //adjusted h
    var h1 = computeH(a1, p1.bStar);
    var h2 = computeH(a2, p2.bStar);

    
    // deltas
    var dh;
    if (h2 - h1 > 180)  
        dh = h2 - h1 - 360;
    else if (h2 - h1 < -180) 
        dh = h2 - h1 + 360 ;
    else 
        dh = h2 - h1;
    

    var dl = p2.LStar - p1.LStar;
    var dc = c2Tick - c1Tick;
    var dBigH = (2 * Math.sqrt(c1Tick * c2Tick) * Math.sin(toRadians(dh / 2)));

    // averages
    var lTickAvg = (p1.LStar + p2.LStar) / 2;
    var cTickAvg = (c1Tick + c2Tick) / 2;

    var hTickAvg;
    if (c1Tick * c2Tick == 0)
        hTickAvg = h1 + h2;
    
    else if (Math.abs(h2 - h1) <= 180) 
        hTickAvg = (h1 + h2) / 2;
    
    else if (h2 + h1 < 360) 
        hTickAvg = (h1 + h2) / 2 + 180;
    
    else 
        hTickAvg = (h1 + h2) / 2 - 180;
   
    
    var l50 = Math.pow(lTickAvg - 50,2);
    var sl = 1 + (0.015 * l50 / Math.sqrt(20 + l50));
    
    var sc = 1 + 0.045 * cTickAvg;
    var t = 1 - 0.17 * Math.cos(toRadians(hTickAvg - 30)) + 0.24 * 
            Math.cos(toRadians(2 * hTickAvg)) + 0.32 * 
            Math.cos(toRadians(3 * hTickAvg + 6)) - 0.2 * 
            Math.cos(toRadians(4 * hTickAvg - 63));

    var sh = 1 + 0.015 * cTickAvg * t;

    var dTheta = 30 * Math.exp(-1 * Math.pow((hTickAvg - 275) / 25 , 2));
    var rc = 2 * Math.sqrt(Math.pow(cTickAvg , 7) / (Math.pow(cTickAvg , 7) + kp));
    var rt = -Math.sin(toRadians(2 * dTheta)) * rc;
    var dlk = dl / sl / kl;
    var dck = dc / sc / kc;
    var dhk = dBigH / sh / kh;
    return Math.sqrt(dlk *dlk + dck *dck + dhk *dhk + rt * dck * dhk);
    
}
function computeH(a , b ) {
    if (a == 0 && b == 0)
        return 0;
    else if (b < 0) 
          return fromRadians(Atan2(a,b)) + 360 ;

    else
        return fromRadians(Atan2(a,b))  ;   
}

function lchToLab (p) { 
    var h = toRadians(p.hStar);
    p.aStar = Math.cos(h) * p.cStar;
    p.bStar = Math.sin(h) * p.cStar;
    return p;
}

function labxyzCorrection(x ) {
    if (Math.pow(x , 3) > 0.008856)
        return Math.pow(x , 3);
    else
       return (x - 16 / 116) / 7.787; 
}
function lchToRgb(p) {
    return xyzToRgb(labToXyz(lchToLab(p)));
}
function labToXyz(p) {
    
    p.y = (p.LStar + 16) / 116;
    p.x = p.aStar / 500 + p.y;
    p.z = p.y - p.bStar / 200;
    
    p.x = labxyzCorrection(p.x) * ECOMPARECOLOR.whiteX;
    p.y = labxyzCorrection(p.y) * ECOMPARECOLOR.whiteY;
    p.z = labxyzCorrection(p.z) * ECOMPARECOLOR.whiteZ;
    return p;
}


function xyzrgbCorrection(x) {
   if (x > 0.0031308) 
        return 1.055 * (Math.pow(x , (1 / 2.4))) - 0.055;
   else
       return 12.92 * x;
       
}
function xyzToRgb(p) {

    var x = p.x / 100, y = p.y / 100 ,z = p.z / 100;
    
    var x1 = x * 0.8951 + y * 0.2664 + z * -0.1614;
    var y1 = x * -0.7502 + y * 1.7135 + z * 0.0367;
    var z1 = x * 0.0389 + y * -0.0685 + z * 1.0296;
    
    var x2 = x1 * 0.98699 + y1 * -0.14705 + z1 * 0.15997;
    var y2 = x1 * 0.43231 + y1 * 0.51836 + z1 * 0.04929;
    var z2 = x1 * -0.00853 + y1 * 0.04004 + z1 * 0.96849;

    r = xyzrgbCorrection(x2 * 3.240479 + y2 * -1.53715 + z2 * -0.498535);
    g = xyzrgbCorrection(x2 * -0.969256 + y2 * 1.875992 + z2 * 0.041556);
    b = xyzrgbCorrection(x2 * 0.055648 + y2 * -0.204043 + z2 * 1.057311);
    
    var c = RGB(Math.min(255, Math.max(0, CLng(r * 255))), 
                   Math.min(255, Math.max(0, CLng(g * 255))), 
                   Math.min(255, Math.max(0, CLng(b * 255))));

   return c;
}

function rgbToLch(rgbColor) {
    //convert from cieL*a*b* to cieL*CH
    //adapted from http://www.brucelindbloom.com/index.html?Equations.html

    var p = rgbToLab(rgbColor);
    if (rgbColor == 0 )
        p.hStar = 0 ;
    else {
        p.hStar =Atan2(p.aStar, p.bStar);
        if (p.hStar > 0) 
            p.hStar = fromRadians(p.hStar);
        else
            p.hStar = 360 - fromRadians(Math.abs(p.hStar));
      
    }
    p.cStar = Math.sqrt(p.aStar * p.aStar + p.bStar * p.bStar);
    return p;
}
function colorPropBigger(a, b, byProp) {

  DebugAssert (a.hasOwnProperty(byProp) && b.hasOwnProperty(byProp),"unknown color prop in sort " + byProp);
  return a[byProp] > b[byProp];
}
function rgbWashout(rgbColor) {

    var p = makeColorProps(rgbColor);
    p.saturation = p.saturation * 0.2;
    p.lightness = p.lightness * 0.9;
    return hslToRgb(p)
}


function sortColorProp(pArray, byProp,optDescending ) {
  descending = fixOptional (optDescending ,false) ? -1 : 1;
  return pArray.sort (function (a,b) {
      var result = a[byProp] > b[byProp] ? 1 : a[byProp] < b[byProp] ? -1 : 0 ;
      return  descending * result ; } );
}

function makeAPalette(rgbColor, optModel, optiType, optHowMany, optDescending) {
  
  var model = fixOptional(optModel, "lch");
  var iType = fixOptional(optiType, "hue");
  var howMany = fixOptional(optHowMany, 5);
  var ph,ps,pl,pf,h,pv,a=[],g;
  
  
  if (model == "lch") {
    ph = "hStar", ps = "cStar", pl = "LStar", pf = lchToRgb;
  }
  else {
    ph = "hue", ps = "saturation", pl = "lightness", pf = hslToRgb;
  }
  
  var top = (iType == "hue" ? 360 : 100);
  g = top / howMany;
  pv = (iType == "hue" ? ph : (iType == "saturation" ? ps : pl));
  var p = makeColorProps(rgbColor); 
  h = p[pv];
  
  // do a number of equally spaced props and store in array
  for (var i =0;i < howMany ;i++ ) {
    if (h > top) h -= top;
    // store new value
    p[pv] =h;
    // convert back to rgb and redo
    p =makeColorProps(pf(p));
    a.push(p);
    // make a new copy
    p = makeColorProps(p.rgb);
    h += g;
  }
  
  return sortColorProp (a, pv, optDescending);
  
}

function arrayLength(a) {
  return a.length;
}

For more color stuff, see Color Fiesta