UNPKG

string-similarity-coloring

Version:

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

136 lines 5.43 kB
/* * 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. */ import Debug from 'debug'; import ss from 'string-similarity'; import convert from 'color-convert'; import { hasColorSet } from './options'; import defaults, { defaultFor } from './defaults'; const debug = Debug('string-similarity-coloring'); function stringHash(str, prefix) { let hash = 0; const L = Math.min(str.length, prefix); for (let 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) { const match = state.primaries.length === 0 ? { bestMatch: undefined } : ss.findBestMatch(str, state.primaries); const { bestMatch } = match; if (!bestMatch || bestMatch.rating === 0) { // no good matches // reserve a primary color from the ColorSet // const primary = state.primaries.length // <-- round robin color assignment const primary = stringHash(str, 3) % colorSet.length; const secondary = state.primaryPopulation[primary]; if (secondary === 0) { const color = colorSet[primary][0]; state.tmp[str] = originalIdx; state.primaries.push(str); state.primaryPopulation[primary]++; state.assignment[originalIdx] = { primary, secondary, 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 const newPrimary = (primary + 1) % colorSet.length; let secondary; let isRandomAssignment = false; if (state.primaryPopulation[newPrimary] === 0) { // then our random hop found an empty primary state.primaries.push(str); secondary = 0; } else { const primaryOriginalIdx = state.assignment.findIndex(_ => _.primary === newPrimary); const { bestMatch } = ss.findBestMatch(str, [allStrings[primaryOriginalIdx]]); // use distance from primary as index into secondary color secondary = ~~(bestMatch.rating * colorSet[newPrimary].length); isRandomAssignment = true; } const color = colorSet[newPrimary][secondary]; state.tmp[str] = originalIdx; state.assignment[originalIdx] = { primary: newPrimary, secondary, color, isRandomAssignment }; debug('assigning random primary', str, newPrimary, secondary, color, match); } } else { // we found a good match! const primaryOriginalIdx = state.tmp[bestMatch.target]; const { primary, color: primaryColor } = state.assignment[primaryOriginalIdx]; // use distance from primary as index into secondary color const secondary = ~~(bestMatch.rating * colorSet[primary].length); const color = colorSet[primary][secondary]; state.primaryPopulation[primary]++; state.assignment[originalIdx] = { primary, secondary, 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 * */ export default function colorize(A, options) { const colorSet = hasColorSet(options) ? options.colorSet : options && options.theme ? defaultFor(options.theme) : defaults; return A .reduce((state, str, idx) => assignColor(str, idx, state, colorSet, A), newStateFor(colorSet)) .assignment .map(_ => Object.assign(_, { color: `#${convert.hsl.hex([_.color.hue, _.color.saturation, _.color.lightness])}` })); } //# sourceMappingURL=index.js.map