//VERSION=3
/*
Perceptually-Uniform Colormap Kit
author: Keenan Ganz (ganzk@rpi.edu)
September 2020
*//*
Reference white values for D65 illuminant,
secondary observer.
*/varREF_X=95.0489varREF_Y=100.0varREF_Z=108.8840functionpercent_between(min,max,x){return(x-min)/(max-min)}functionclip(min,max,x){returnMath.min(max,Math.max(min,x))}functionrgb2lab(rgb){/*
Convert rgb coordinates to CIELAB coordinates via XYZ.
Expects normalized RGB values.
Arithmetic from easyrgb.com
*/let[r,g,b]=rgbfunctionto_linear(val){if(val>0.04045)returnMath.pow((val+0.055)/1.055,2.4)elsereturnval/12.92}letr_lin=to_linear(r)*100letg_lin=to_linear(g)*100letb_lin=to_linear(b)*100letx=r_lin*0.4124+g_lin*0.3576+b_lin*0.1805lety=r_lin*0.2126+g_lin*0.7152+b_lin*0.0722letz=r_lin*0.0193+g_lin*0.1192+b_lin*0.9505letx_std=x/REF_Xlety_std=y/REF_Yletz_std=z/REF_Zfunctionstd_prep(val){if(val>0.008856)returnMath.pow(val,1.0/3.0)elsereturnval*7.787+(16.0/116.0)}letL=116.0*(std_prep(y_std)-16.0/116.0)leta=500.0*(std_prep(x_std)-std_prep(y_std))b=200.0*(std_prep(y_std)-std_prep(z_std))return[L,a,b]}functionlab2rgb(Lab){/*
Convert CIELAB coordinates to RGB coordinates.
Arithmetic from easyrgb.com
*/let[L,a,b]=Labletvar_y=(L+16.0)/116.0letvar_x=(a/500.0)+var_yletvar_z=var_y-(b/200.0)functionundo_std_prep(val){if(Math.pow(val,3.0)>0.008856)returnMath.pow(val,3.0)elsereturn(val-(16.0/116.0))/7.787}var_y=undo_std_prep(var_y)var_x=undo_std_prep(var_x)var_z=undo_std_prep(var_z)letx=var_x*REF_X/100lety=var_y*REF_Y/100letz=var_z*REF_Z/100letvar_r=x*3.2406+y*-1.5372+z*-0.4986letvar_g=x*-0.9689+y*1.8758+z*0.0415letvar_b=x*0.0557+y*-0.2040+z*1.0570functionundo_linear(val){if(val>0.0031308)return1.055*Math.pow(val,(1.0/2.4))-0.055elsereturnval*12.92}letr=Math.max(undo_linear(var_r),0)letg=Math.max(undo_linear(var_g),0)b=Math.max(undo_linear(var_b),0)// mapping isn't perfect, constrain to [0, 1]
return[clip(0,1,r),clip(0,1,g),clip(0,1,b)]}classColormap{/*
Base class for making perceptually uniform color maps. Using this class on its own
simply maps RGB coordinates to LAB space and linearly interpolates inbetween values.
Use SequentialColorMap, DivergentColorMap, etc. for more specific use cases.
*/constructor(color_anchors,data_anchors,remap=true,uniform=false){if(color_anchors.length<1)throw"ColorMap requires at least one color."if(color_anchors.length!=data_anchors.length)throw"Color and data anchors must be of same length."// verify that the data array is sorted low to high
for(leti=1;i<data_anchors.length;i++){if(data_anchors[i]<data_anchors[i-1])throw"Data anchors array must be sorted."}// map incoming rgb coordinates into LAB space
this.data_anchors=data_anchorsif(remap)this.color_anchors=color_anchors.map(rgb2lab)elsethis.color_anchors=color_anchors// do the lightness correction, if desired, and then check
// if the correction moved colors outside of RGB space
if(uniform){this._lightness_correction()}}_lightness_correction(){return}get_color(data_value){// return edge values if data value is oob
if(data_value<=this.data_anchors[0])returnlab2rgb(this.color_anchors[0])elseif(data_value>=this.data_anchors[this.data_anchors.length-1])returnlab2rgb(this.color_anchors[this.color_anchors.length-1])returnlab2rgb(colorBlend(data_value,this.data_anchors,this.color_anchors))}}classLinearColormapextendsColormap{/*
Simple linear ramp color map class. Set uniform to true
in the constructor to enforce constant lightness.
*/constructor(color_anchors,data_anchors,uniform=true){super(color_anchors,data_anchors,true,uniform)}_lightness_correction(){// get overall change in lightness
letL0=this.color_anchors[0][0]letLp=this.color_anchors[this.color_anchors.length-1][0]letdL=Lp-L0// make the lightness values monotonically change
for(leti=1;i<this.color_anchors.length-1;i++){letpercent_interval=percent_between(this.data_anchors[this.data_anchors.length-1],this.data_anchors[0],this.data_anchors[i])this.color_anchors[i][0]=L0+(dL*percent_interval)}}};classDivergentColormapextendsColormap{/*
Color map that reaches a max/min lightness at its center and changes
monotonically toward the edges of the data with equal lightness
at each edge. If you don't want the lightness correction, just use
LinearColormap with uniform=false.
*/constructor(color_anchors,data_anchors){if(color_anchors.length%2!=1)throw"DivergentColorMap must have an odd number of anchors."super(color_anchors,data_anchors,true,true)}_lightness_correction(){// L0 is the mean lightness of the edge colors
// Lp is the lightness of the center color
letlen=this.color_anchors.lengthletL0=(this.color_anchors[0][0]+this.color_anchors[len-1][0])/2// set the edge colors lightness to the mean
this.color_anchors[len-1][0]=this.color_anchors[0][0]=L0// Calculate intermediate lightness values
letLp=this.color_anchors[Math.floor(len/2)][0]letdL=Lp-L0for(leti=1;i<Math.floor(len/2);i++){letleft_percent_interval=percent_between(this.data_anchors[Math.floor(len/2)],this.data_anchors[0],this.data_anchors[i])// Invert right % since lightness trends the other way now
letright_percent_interval=1-percent_between(this.data_anchors[len-1],this.data_anchors[Math.floor(len/2)],this.data_anchors[len-1-i])this.color_anchors[i][0]=L0+(dL*left_percent_interval)this.color_anchors[len-1-i][0]=L0+(dL*right_percent_interval)}}}classIsoluminantColormapextendsColormap{/*
Color map that enforces constant lightness. Not particularly pretty on its own,
but can be overlaid on relief shading to visualize data on top of topography.
*/constructor(color_anchors,data_anchors){super(color_anchors,data_anchors,true,true)}_lightness_correction(){// get the mean lightness of all colors
letL_sum=0for(leti=0;i<this.color_anchors.length;i++){L_sum+=this.color_anchors[i][0]}letL_avg=L_sum/this.color_anchors.length// set all anchors to have lightness of this value
for(leti=0;i<this.color_anchors.length;i++){this.color_anchors[i][0]=L_avg}}}/*
Example usage: Masked NDVI on a white-green
color map.
*/varmap=[[217/255,229/255,206/255],[12/255,22/255,3/255],]vardata=[-0.1,1]varcmap=newLinearColormap(map,data);functionsetup(){return{input:["B02","B03","B04","B08","dataMask"],output:{bands:3}};}functiontrueColor(sample){return[sample.B04*2.5,sample.B03*2.5,sample.B02*2.5]}functionevaluatePixel(sample){letndvi=(sample.B08-sample.B04)/(sample.B04+sample.B08)if(ndvi>-.1){returncmap.get_color(ndvi)}else{returntrueColor(sample)}}
A set of visualization utilities that produce beautiful images designed for human perception from single-channel data (NDVI, spectral angle, etc). Generate quality figures that are true to the data.
Good visualizations depend on good color maps. However, it is very easy to unintentionally create data artifacts or obscure trends unless care is taken. For example, many have written about the problems with rainbow color maps (e.g. Borland and Taylor 2007 and Kovesi 2015, see references). Yet, rainbows remain prevalent in scientific publications (and occasionally on SentinelHub).
SentinelHub custom scripts have limited support for custom color maps: colorBlend and ColorRampVisualizer. As a result, many users end up hardcoding their colors. Also, these functions use RGB colors instead of a color gamut designed around human perception. The goal of this project is to create tools for custom script developers to generate useful and perceptually uniform color maps. It provides visualization classes for users familiar with CIELAB color space and works to correct colors in RGB space to be perceptually uniform. The underlying mathematics is used by many scientific plotting applications, such as matplotlib, bokeh, chroma.js, and others.
Details of the script
How the Script Works
This script supports 4 flavors of color map in addition to functions that inter-convert between the CIELAB, XYZ, and RGB color spaces in pure JavaScript. In each case, the user simply provides two arrays, one for anchor colors and one for data values, and the class will generate the correct color for data values calculated by the script. For example, to generate a brown-green color map for NDVI values in the range [0, 1], simply use:
var ndvi_map = [
// https://www.google.com/search?q=rgb+color+picker
[66/255, 50/255, 28/255],
[0, 209/255, 3/255],
]
var data = [0, 1]
var cmap = new LinearColormap(ndvi_map, data);
function evaluatePixel(sample) {
let ndvi = (sample.B08 - sample.B04) / (sample.B08 + sample.B04)
return cmap.get_color(ndvi)
}
In addition to LinearColormap, there is DivergentColormap, IsoluminantColormap, and the base class Colormap. The base class expects colors in CIELAB color space and simply calls colorBlend to get in-between colors before converting back to RGB space. All other classes expect colors in RGB space, map them onto CIELAB space, and apply a lightness correction specific to the class. The corrections for each class are:
LinearColormap: Lightness monotonically changes across the whole map. Your st