All files / owid-grapher/grapher/color CategoricalColorAssigner.ts

100% Statements 59/59
50% Branches 3/6
100% Functions 3/3
100% Lines 59/59

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 871x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 2x 2x 2x 2x 2x 1x 1x 5x 5x 5x 5x 5x 5x 1x 1x 5x 5x                                                        
import { Color } from "../../clientUtils/owidTypes"
import { last } from "../../clientUtils/Util"
import { ColorScheme } from "./ColorScheme"
import { getLeastUsedColor } from "./ColorUtils"
 
type CategoryId = string
export type CategoricalColorMap = Map<CategoryId, Color>
export type CategoricalColorMapReadonly = ReadonlyMap<CategoryId, Color>
 
export interface CategoricalColorAssignerProps {
    colorScheme: ColorScheme
    invertColorScheme?: boolean
 
    /** The custom color mappings (most likely author-specified) to use. */
    colorMap?: CategoricalColorMap
 
    /**
     * A cache for custom colors or automatically selected colors for each identifier
     * encountered.
     *
     * In the Grapher, this is persisted across charts, so that a line chart
     * that turns into a bar chart will have a matching color scheme across
     * both states.
     */
    autoColorMapCache?: CategoricalColorMap
}
 
/**
 * Assigns custom categorical colors, e.g. specified by an author for entities or variables.
 *
 * When an identifier doesn't have an assigned color, it uses the least used color in the scheme.
 *
 * Keeps a cache so that identical identifiers are assigned consistent colors. See
 * Grapher#seriesColorMap for an example of a cache.
 */
export class CategoricalColorAssigner {
    private colorScheme: ColorScheme
    private invertColorScheme: boolean
    private colorMap: CategoricalColorMapReadonly
    private autoColorMapCache: CategoricalColorMap
 
    constructor(props: CategoricalColorAssignerProps) {
        this.colorScheme = props.colorScheme
        this.invertColorScheme = props.invertColorScheme ?? false
        this.colorMap = props.colorMap ?? new Map()
        this.autoColorMapCache = props.autoColorMapCache ?? new Map()
    }
 
    private get usedColors(): Color[] {
        const merged: CategoricalColorMap = new Map([
            ...this.autoColorMapCache,
            ...this.colorMap,
        ])
        return Array.from(merged.values())
    }
 
    private get availableColors(): Color[] {
        // copy the colors array because we might need to reverse it
        const colors = last(this.colorScheme.colorSets)?.slice() ?? []
        if (this.invertColorScheme) colors.reverse()
        return colors
    }
 
    private get leastUsedColor(): Color {
        const leastUsedColor = getLeastUsedColor(
            this.availableColors,
            this.usedColors
        )
        // TODO handle this better?
        if (leastUsedColor === undefined) {
            console.trace("Least used color is undefined, using black.", {
                availableColors: this.availableColors,
                usedColors: this.usedColors,
            })
        }
        return leastUsedColor ?? "#000"
    }
 
    assign(id: CategoryId): Color {
        let color = this.colorMap.get(id)
        if (color === undefined) color = this.autoColorMapCache.get(id)
        if (color === undefined) color = this.leastUsedColor
        this.autoColorMapCache.set(id, color)
        return color
    }
}