@legumeinfo/taxon-symbology
Version:
A TypeScript library for consistent coloring of visualizations by genus & species
108 lines • 3.6 kB
JavaScript
/*
* use the chroma.js library to colorize taxons consistently and predictably.
* e.g. Always returns same hue for Arachis, but scaled lightness depending on
* the Species -- completely abitrarily, but consistently.
*
* taxonChroma.get(taxon, options);
*
* // examples:
* taxonChroma.get('Arachis hypogaea');
* taxonChroma.get('Arachis burkartii');
*
* // make 20% lighter overall
* taxonChroma.get(someTaxon, { lightnessFactor: 1.2 } );
*
* // override some taxon
* taxonChroma.get(acc.properties.taxon, {
* 'lightnessFactor' : 1.2,
* 'overrides' : {
* 'phaseolus lunatus' : 'green',
* }
* });
*/
import * as chroma from 'chroma.ts';
// New colors 15 degrees apart in hue space, to reduce conflicts
const baseColors = {
acacia: '#ffc000',
aeschynomene: '#ff4000',
apios: '#00c0ff',
arachis: '#ff8000',
bauhinia: '#ff00c0',
cajanus: '#0040ff',
cercis: '#ff0040',
chamaecrista: '#ffff00',
cicer: '#80ff00',
dalbergia: '#ff0000',
glycine: '#4000ff',
lablab: '#8000ff',
lens: '#00ff80',
lotus: '#00ffff',
lupinus: '#c000ff',
medicago: '#40ff00',
phanera: '#ff0080',
phaseolus: '#0000ff',
pisum: '#00ff00',
quillaja: '#666666', // not a legume, but related
senna: '#c0ff00',
sindora: '#ff00ff',
trifolium: '#00ff40',
vicia: '#00ffc0',
vigna: '#0080ff',
};
export const genera = Object.keys(baseColors);
/**
* a consistent hashing algorithm
* https://gist.github.com/vaiorabbit/5657561
* http://isthe.com/chongo/tech/comp/fnv/#xor-fold
**/
function fnv32a(str, hashSize) {
const FNV1_32A_INIT = 0x811c9dc5;
let hval = FNV1_32A_INIT;
for (let i = 0; i < str.length; ++i) {
hval ^= str.charCodeAt(i);
hval +=
(hval << 1) + (hval << 4) + (hval << 7) + (hval << 8) + (hval << 24);
}
return (hval >>> 0) % hashSize;
}
export class TaxonChroma {
constructor(defaultColor = '#d3d3d3') {
this.colorCache = {};
this.defaultColor = defaultColor;
}
get(taxon, options = {}) {
if (taxon.indexOf(' ') === -1) {
throw 'Error: required format is "genus species" with a space between';
}
taxon = taxon.toLowerCase();
// options is an object w/ keys lightnessFactor, overrides
const { lightnessFactor = TaxonChroma.LIGHTNESS_FACTOR, overrides = {} } = options;
// convert overrides to lowercase
const lowerOverrides = Object.fromEntries(Object.entries(overrides).map(([k, v]) => [k.toLowerCase(), v]));
// return override color
if (lowerOverrides[taxon] !== undefined) {
return lowerOverrides[taxon];
}
// return cached color
if (this.colorCache[taxon] !== undefined) {
return this.colorCache[taxon];
}
// compute color
let color = this.defaultColor;
const [genus, species, ..._] = taxon.split(' ');
if (genus in baseColors) {
const genusColor = baseColors[genus];
const [hue, ..._] = chroma.color(genusColor).hsl();
const lightness = TaxonChroma.MIN_LIGHTNESS +
(fnv32a(species, 1000) / 1000) * (1 - 2 * TaxonChroma.MIN_LIGHTNESS);
color = chroma.color(hue, 1, lightness * lightnessFactor, 'hsl').hex();
}
this.colorCache[taxon] = color;
return color;
}
}
// default lightness factor (1= don't post-adjust)
TaxonChroma.LIGHTNESS_FACTOR = 1;
TaxonChroma.MIN_LIGHTNESS = 0.3;
export const taxonChroma = new TaxonChroma();
//# sourceMappingURL=index.js.map