UNPKG

string-similarity-coloring

Version:

Color a given set of N strings into a set of M<N color classes

161 lines 6.89 kB
"use strict"; /* * 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