string-similarity-coloring
Version:
Color a given set of N strings into a set of M<N color classes
161 lines • 6.89 kB
JavaScript
;
/*
* Copyright 2021 IBM
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
var debug_1 = __importDefault(require("debug"));
var string_similarity_1 = __importDefault(require("string-similarity"));
var color_convert_1 = __importDefault(require("color-convert"));
var options_1 = require("./options");
var defaults_1 = __importStar(require("./defaults"));
var debug = debug_1.default('string-similarity-coloring');
function stringHash(str, prefix) {
var hash = 0;
var L = Math.min(str.length, prefix);
for (var idx = 0; idx < L; idx++) {
hash = str.charCodeAt(idx) | hash;
}
return hash;
}
/**
* Update State to assign a color to A[idx]
*
*/
function assignColor(str, originalIdx, state, colorSet, allStrings) {
var match = state.primaries.length === 0
? { bestMatch: undefined }
: string_similarity_1.default.findBestMatch(str, state.primaries);
var bestMatch = match.bestMatch;
if (!bestMatch || bestMatch.rating === 0) {
// no good matches
// reserve a primary color from the ColorSet
// const primary = state.primaries.length // <-- round robin color assignment
var primary = stringHash(str, 3) % colorSet.length;
var secondary = state.primaryPopulation[primary];
if (secondary === 0) {
var color = colorSet[primary][0];
state.tmp[str] = originalIdx;
state.primaries.push(str);
state.primaryPopulation[primary]++;
state.assignment[originalIdx] = {
primary: primary,
secondary: secondary,
color: color,
isRandomAssignment: false
};
debug('assigning new primary', str, color);
}
else {
// no more primary colors left in the given ColorSet
// pick the next one and hope for the best
// we could use a random assignment, or we could scan for an empty color class
// however, we desire consistency; and, if the user calls us with the
// string set in sorted order of importance to them, then we will only
// have conflicts for the less important strings
var newPrimary_1 = (primary + 1) % colorSet.length;
var secondary_1;
var isRandomAssignment = false;
if (state.primaryPopulation[newPrimary_1] === 0) {
// then our random hop found an empty primary
state.primaries.push(str);
secondary_1 = 0;
}
else {
var primaryOriginalIdx = state.assignment.findIndex(function (_) { return _.primary === newPrimary_1; });
var bestMatch_1 = string_similarity_1.default.findBestMatch(str, [allStrings[primaryOriginalIdx]]).bestMatch;
// use distance from primary as index into secondary color
secondary_1 = ~~(bestMatch_1.rating * colorSet[newPrimary_1].length);
isRandomAssignment = true;
}
var color = colorSet[newPrimary_1][secondary_1];
state.tmp[str] = originalIdx;
state.assignment[originalIdx] = {
primary: newPrimary_1,
secondary: secondary_1,
color: color,
isRandomAssignment: isRandomAssignment
};
debug('assigning random primary', str, newPrimary_1, secondary_1, color, match);
}
}
else {
// we found a good match!
var primaryOriginalIdx = state.tmp[bestMatch.target];
var _a = state.assignment[primaryOriginalIdx], primary = _a.primary, primaryColor = _a.color;
// use distance from primary as index into secondary color
var secondary = ~~(bestMatch.rating * colorSet[primary].length);
var color = colorSet[primary][secondary];
state.primaryPopulation[primary]++;
state.assignment[originalIdx] = {
primary: primary,
secondary: secondary,
color: color,
isRandomAssignment: false
};
debug('variant of primary', str, primaryOriginalIdx, color);
}
return state;
}
/** @return empty initial state for the given `ColorSet` */
function newStateFor(colorSet) {
return {
primaries: [],
assignment: [],
tmp: {},
primaryPopulation: new Array(colorSet.length).fill(0)
};
}
/**
* Takes a list of N strings, and returns a parallel list of N
* colors. The number of distinct colors in the return value will be
* M, where M is given by options.colorSet or the default color set,
* which has 6 primary colors, and 4 secondary colors.
*
* @return array of hex strings
*
*/
function colorize(A, options) {
var colorSet = options_1.hasColorSet(options) ? options.colorSet : options && options.theme ? defaults_1.defaultFor(options.theme) : defaults_1.default;
return A
.reduce(function (state, str, idx) { return assignColor(str, idx, state, colorSet, A); }, newStateFor(colorSet))
.assignment
.map(function (_) { return Object.assign(_, {
color: "#" + color_convert_1.default.hsl.hex([_.color.hue, _.color.saturation, _.color.lightness])
}); });
}
exports.default = colorize;
//# sourceMappingURL=index.js.map