I like playing around with colors, and have many articles on this site featuring various techniques. This one is an experiment I’ve been working on to derive colors and color mixtures based on textual content. To start with, I’ll be using a spreadsheet to provide the content but in later articles I might move on to Docs and Slides.

Color from content

The first type of content is where text itself mentions colors. Imagine you have this list of song titles and you want to color the background according to their titles.

We’ll try to interpret the content to look for colors. Some have multiple colors mentioned in a song title – so let’s mix all of the colors mentioned into 1. Notice that the font color has also been adjusted to contrast the background color.

Sort titles by color derived from content

Then sorting it by the combined colors, magically gives a much more interestingly organized list.

Color Saturation to represent content prevalance

Changing the color saturation by how many times a color is mentioned in the content, might be nice too – so Green, Green Grass will be ‘greener’ than say, Green Onions. It’s pretty subtle, but you can see the saturated hex code is a little different – the Green,green,really green green grass would be even greener.

Alternative lists

Of course, you may want to organize content by color that doesn’t itself contain any specific color name references.

You could create alternative lists which reference other lists which are themselves color based. Imagine you had a recipe book, where the ingredients were color coded, and the overall recipe itself could be assigned a color based on the ingredients in it.

Ingredients

Here’s a list of ingredients, sorted by colors. Using the same technique as with the list of song titles, we can approximate the color of an ingredient by listing the colors that seem to make it up. For example an aubergine (eggplant) has both purple and darkgreen shades. I’ll talk about the weight column later.

Recipes

Now here’s a list of recipes, this time showing the ingredients by color, and combining them to provide 2 final colors for the finished recipe.

Weighted mixing

The mix column is all the colors of the ingredients mixed as if they were equal in volume, whereas the mix-saturated column takes account of the volume by multiplying any numbers it finds in the ingredients by the weight in the ingredient column. For example 2 tbsp of olive oil = 2 x 15 = 30g and 300g of mushrooms = 300 x 1 = 300g. It’s far from perfect, but it’s just a proof of concept.

Other applications

Using this concept, any content could be mapped to a list which has color text to be used as a reference. For example, a document with sentiment analysis applied or paragraphs that reference particular subjects and so on.

Code samples

You’ll need to set a few things up first.

bmChroma

To get started you’ll need my bmChroma library. Reference is below.

1zjBPTX8meADK6W2tbw-sNB0479OMN2hhT1O5MGna7v5liAj7paj-W8QE

PreFiddler

I’m using PreFiddler and Fiddler for sheet manipulation as usual. bmPreFiddler id is below (it has its own reference to Fiddler built in).

13JUFGY18RHfjjuKmIRRfvmGlCYrEkEtN6uUm-iLUcxOUFRJD-WBX-tkR

Exports

If you’ve used my libaries before, you’ll probably know I usually add an exports layer like this which not only abstracts whether or not we are using libraries away from the main code, but also deals with the problems you sometimes get with the ordering of script files in project.

As a bonus, it’ll throw an error if you accidentally try to access an unknown property or method. I recommend it for all your projects. This is the one you’ll to add to your project for this article.

var Exports = {

get PreFiddler () {
return bmPreFiddler.PreFiddler
},

newPreFiddler(...args) {
return this.guard(Exports.PreFiddler().getFiddler(...args))
},

get ColorWords () {
return this.guard (bmChroma.Exports.ColorWords)
},

// used to trap access to unknown properties
guard(target) {
return new Proxy(target, this.validateProperties)
},

/**
* for validating attempts to access non existent properties
*/
get validateProperties() {
return {
get(target, prop, receiver) {
// typeof and console use the inspect prop
if (
typeof prop !== 'symbol' &&
prop !== 'inspect' &&
!Reflect.has(target, prop)
) throw `guard detected attempt to get non-existent property ${prop}`

return Reflect.get(target, prop, receiver)
},

set(target, prop, value, receiver) {
if (!Reflect.has(target, prop)) throw `guard attempt to set non-existent property ${prop}`
return Reflect.set(target, prop, value, receiver)
}
}
}

}
Exports

Chroma

Most of the heavy color maths is done using a version of the excellent chroma.js, which is wrapped inside the bmChroma library. Let’s look at creating the list of songs – the first example in this article.

The code

You can take a copy of my test sheet, or make your own. In any case, just update the PreFiddler spec to your own id and sheetName.

// test bmmaterial colors from string
const t6 = () => {

const { getColors, getContrast, getColorPack } = Exports.ColorWords
const fiddler = Exports.newPreFiddler({
id: "1UYuFztwETn7nGctf7vvlsV9lNa5p_hLoKrC5wdDWasg",
sheetName: "songs"
})
const light = '#ffffff'
const dark = '#000000'

// start with a clean sheet
fiddler.getSheet().clearFormats()

// function to create a fiddler sized 2d array
const getEmpty = (fiddler, fill) => Array.from({ length: fiddler.getNumRows() })
.map((_, rn) => Array.from({ length: fiddler.getNumColumns() }).fill(fill))


// start with clean back and font colors
const backgrounds = getEmpty(fiddler, light)
const fontColors = getEmpty(fiddler, dark)

// interesting columns to index the color matrices
// these are the column names
const positions = new Map(fiddler.getHeaders().map((f, i) => [f, i]))

// now patch in the color values
const data = fiddler.getData().map(row => {

// get colors mentioned in this piece of text
const list = getColors(row.text)

// now the mixof all the colors
row.mix = list.mix.color.hex
row['mix-saturated'] = list.mix.saturated.hex
return row
})

// set the range for formattings
const dr = fiddler.getRange().offset(1, 0, fiddler.getNumRows(), fiddler.getNumColumns())
const headBack = getColorPack(getColorPack("orange").base.brighten())

// sort and then apply the colors
fiddler
.setData(data)
.sortFiddler('mix-saturated')
.mapRows((row, { rowOffset }) => {
backgrounds[rowOffset][positions.get('mix-saturated')] = row['mix-saturated']
backgrounds[rowOffset][positions.get('mix')] = row.mix
backgrounds[rowOffset][positions.get('text')] = row['mix-saturated']

// aply the contrasts in one shot
fontColors[rowOffset] = backgrounds[rowOffset].map(f => getContrast(f, { dark, light }))
return row
})
.setHeaderFormat({
wraps: true,
backgrounds: headBack.hex,
fontColors: headBack.contrast
})
.dumpValues()

// finally, apply the formats
dr.setBackgrounds(backgrounds)
dr.setFontColors(fontColors)


}
Song wrapping

Most of the code is related to populating the sheet, which you can read about in the write ups for PreFiddler and Fiddler.

bmChroma

In this article, I’ll concentrate on the methods available from bmChroma

const { getColors, getContrast, getColorPack, discover, getContrast,getChroma,mixer } = Exports.ColorWords

discover ( text ) : ColorNames

Pass a string which may or may not contain various words that can be recognized as colors. Currently this is any hex code, or any of the standard X11 color names. In a future article I’ll add Material colors to the list if there’s any interest in this library.

It will return a TextToColors object, which looks like this.

/**
 * @typedef ColorNames
 * @property {string} text the original text
 * @property {string[]} colorNames the words in the string that could be colors
 */ 

getColorPack (color) : ColorPack

Pass a color in any format to and receive a ColorPack, which contains extended information about the color, as well as a property which is chroma color object.

/**
 * @typedef ColorPack
 * @property {object} base a chroma color
 * @property {string} hex version of the based (same as base.hex())
 * @property {string} contrast the contrast color in hex
 * @property {string} name the name of the color (or a hex code if it cant be named)
 */

You can use the base property to access all the chroma.color native if you need to tweak the standard returns

const red =  getColorPack("red")
const backgroundColor = red.hex
const fontColor = red.contrast
const lightRed = red.base.brighten(2).hex()
base property gives access to all chroma.color methods

getContrast (color, {dark = ‘#000000’, light = ‘#ffffff’, mode =”rgb”, minLuminance = 0.5} )

A ColorPack already has a contrast property – the hex value to use for contrasting text, and you can always use getColorPack to create one. You can also use getContrast directly if you want to change the luminance threshold (between 0 and 1 – the point at which it flips from providing a dark color to a light color), the mode (the color system used to calculate the luminance eg ‘lch’ or ‘hsl’), or the values to use for light or dark.

const red =  getColorPack("red")
const backgroundColor = red.hex

// fontcolor1 === fontcolor2
const fontColor1 = red.contrast
const fontColor2 = getContrast (red.hex)

// fontColor3 === fontColor4
const lightRed = getColorPack (red.base.brighten(2).hex())
const lightBackgroundColor = lightRed.hex

const fontColor3 = lightRed.contrast
const fontColor4 = getContrast (lightRed.hex)

// fontColor5 !== fontColor3/4
const fontColor5 = getContrast (lightRed.hex, {
dark: "DarkSlateGray",
light: "FloralWhite"
})

// fontColor6 may be equal to fontColor5
const fontColor6 = getContrast (lightRed.hex, {
dark: "DarkSlateGray",
light: "FloralWhite",
luminance: 0.4,
mode: "lch"
})
using getContrast

getChroma () : chroma

You may want to have direct access to the chroma API. getChroma() will return an instance you can use directly.

mixer ({colors, weights, mode=’rgb’}) : ColorPack

You can use this to mix an array of colors an end up with a single color that blends them all. Weights is an optional array the same length as the colors array, which allows you to weight the blending process to certain colors. If there are no weights provided, all colors are treated as equal. The mode picks the color space to use to guide the blending, eg ‘lch’.

const backgroundColor = mixer ({
colors: ['red', 'blue'],
weights: [5,1]
})
mixing colors

getColors (text)

This is the main interface used to process documents.

Text contains a series of colors for example

const result = getColors("The green, green grass of home")

will produce this result

{
"text": "The green, green grass of home",
"colorNames": ["green", "green"],
"colorPacks": [{
"base": {...a chroma.color object},
"hex": "#008000",
"contrast": "#ffffff",
"name": "green"
}, {
"base": {...a chroma.color object},
"hex": "#008000",
"contrast": "#ffffff",
"name": "green"
}],
"mentions": [{
"color": {
"base": {...a chroma.color object},
"hex": "#008000",
"contrast": "#ffffff",
"name": "green"
},
"noticed": [{
"base": {...a chroma.color object},
"hex": "#008000",
"contrast": "#ffffff",
"name": "green"
}, {
"base": {...a chroma.color object},
"hex": "#008000",
"contrast": "#ffffff",
"name": "green"
}],
"saturated": {
"base": {...a chroma.color object},
"hex": "#008300",
"contrast": "#ffffff",
"name": "#008300"
}
}],
"mix": {
"base": {...a chroma.color object},
"hex": "#008000",
"contrast": "#ffffff",
"name": "green"
},
"saturated": {
"base": {...a chroma.color object},
"hex": "#008300",
"contrast": "#ffffff",
"name": "#008300"
}
}
}
example getColors response

Result properties

text

A repeat of the original text

colorNames

An array of all the recognized color names

colorPacks

An array of ColorPacks corresponding with the colorNames array

mentions

A consolidated array of each unique color mentioned

mentions.color

A ColorPack of each color mentioned

mentions.noticed

An array of ColorPacks corresponding to each time the the mentions.color has been noticed

mix

A ColorPack with an unweighted mix of all colors mentioned

saturated

A ColorPack with a mix of all colors mentioned weighted by how often they occur.

Links

bmChroma 1zjBPTX8meADK6W2tbw-sNB0479OMN2hhT1O5MGna7v5liAj7paj-W8QE (github)

bmPreFiddler 13JUFGY18RHfjjuKmIRRfvmGlCYrEkEtN6uUm-iLUcxOUFRJD-WBX-tkR (github)

testBmYiq Github

Next

If there’s an interest in this library, I’ll add features such as material color recognition and examples using formats other than spreadsheets, or other interesting suggestions you may have.

Related

Contrast colors with Yiq

PreFiddler and Fiddler

chroma