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

84.95% Statements 175/206
42.11% Branches 8/19
83.33% Functions 5/6
84.95% Lines 175/206

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 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 2331x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 452x 452x 452x 452x 452x 452x 452x 452x 452x 452x 452x 452x 452x 452x 452x 452x 452x 452x 452x 452x 452x 452x 452x 452x 452x 452x 452x 452x 452x 452x 452x 452x 452x 452x 452x 452x 452x 452x 452x 452x 452x 452x 452x 452x 452x 452x 452x 452x 452x 452x 452x 452x 452x 452x 452x 452x 452x 452x 452x 452x 452x 452x 452x 452x 452x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 10x 10x 10x 10x 10x 1x 1x 442x 442x 442x 1x 1x 63x 63x 63x 63x 63x 63x 63x     16x 16x 16x 16x 16x 16x 16x 16x 16x 16x 63x 63x 63x 63x   63x 63x 63x 63x 63x 63x 63x 63x 63x 63x 63x 63x     6x 6x 6x 6x 6x 6x 6x 6x 63x     63x 63x 63x 63x   63x   63x 63x 63x 63x 63x 63x 63x 63x 63x 63x 63x 63x 63x 63x 63x 63x 63x 63x 63x 63x 63x 63x 63x 1x 1x                                                                                                  
import { observable } from "mobx"
import { Color } from "../../coreTable/CoreTableConstants"
import { ColumnColorScale } from "../../coreTable/CoreColumnDef"
import {
    deleteRuntimeAndUnchangedProps,
    objectWithPersistablesToObject,
    Persistable,
    updatePersistables,
} from "../../clientUtils/persistable/Persistable"
import { extend, isEmpty, trimObject } from "../../clientUtils/Util"
import { ColorSchemeName } from "./ColorConstants"
import { BinningStrategy } from "./BinningStrategy"
import { NO_DATA_LABEL } from "./ColorScale"
 
export class ColorScaleConfigDefaults {
    // Color scheme
    // ============
 
    /** Key for a colorbrewer scheme */
    @observable baseColorScheme?: ColorSchemeName
 
    /** Reverse the order of colors in the color scheme (defined by `baseColorScheme`) */
    @observable colorSchemeInvert?: boolean = undefined
 
    // Numeric bins
    // ============
 
    /** The strategy for generating the bin boundaries */
    @observable binningStrategy: BinningStrategy = BinningStrategy.ckmeans
    /** The *suggested* number of bins for the automatic binning algorithm */
    @observable binningStrategyBinCount?: number
 
    /** The minimum bracket of the first bin */
    @observable customNumericMinValue?: number
    /** Custom maximum brackets for each numeric bin. Only applied when strategy is `manual`. */
    @observable customNumericValues: number[] = []
    /**
     * Custom labels for each numeric bin. Only applied when strategy is `manual`.
     * `undefined` or `null` falls back to default label.
     * We need to handle `null` because JSON serializes `undefined` values
     * inside arrays into `null`.
     */
    @observable customNumericLabels: (string | undefined | null)[] = []
 
    /** Whether `customNumericColors` are used to override the color scheme. */
    @observable customNumericColorsActive?: boolean = undefined
    /**
     * Override some or all colors for the numerical color legend.
     * `undefined` or `null` falls back the color scheme color.
     * We need to handle `null` because JSON serializes `undefined` values
     * inside arrays into `null`.
     */
    @observable customNumericColors: (Color | undefined | null)[] = []
 
    /** Whether the visual scaling for the color legend is disabled. */
    @observable equalSizeBins?: boolean = undefined
 
    // Categorical bins
    // ================
 
    @observable.ref customCategoryColors: {
        [key: string]: string | undefined
    } = {}
 
    @observable.ref customCategoryLabels: {
        [key: string]: string | undefined
    } = {}
 
    // Allow hiding categories from the legend
    @observable.ref customHiddenCategories: {
        [key: string]: true | undefined
    } = {}
 
    // Other
    // =====
 
    /** A custom legend description. Only used in ScatterPlot legend titles for now. */
    @observable legendDescription?: string = undefined
}
 
export type ColorScaleConfigInterface = ColorScaleConfigDefaults
 
export class ColorScaleConfig
    extends ColorScaleConfigDefaults
    implements Persistable
{
    updateFromObject(obj: any): void {
        extend(this, obj)
    }
 
    toObject(): ColorScaleConfigInterface {
        const obj: ColorScaleConfigInterface =
            objectWithPersistablesToObject(this)
        deleteRuntimeAndUnchangedProps(obj, new ColorScaleConfigDefaults())
        return trimObject(obj)
    }
 
    constructor(obj?: Partial<ColorScaleConfig>) {
        super()
        updatePersistables(this, obj)
    }
 
    static fromDSL(scale: ColumnColorScale): ColorScaleConfig | undefined {
        const colorSchemeInvert = scale.colorScaleInvert
        const baseColorScheme = scale.colorScaleScheme as ColorSchemeName
 
        const customNumericValues: number[] = []
        const customNumericLabels: (string | undefined)[] = []
        const customNumericColors: (Color | undefined)[] = []
        scale.colorScaleNumericBins
            ?.split(INTER_BIN_DELIMITER)
            .forEach((bin): void => {
                const [value, color, ...label] = bin.split(
                    INTRA_BIN_DELIMITER
                ) as (string | undefined)[]
                if (!value) return
                customNumericValues.push(parseFloat(value))
                customNumericColors.push(color?.trim() || undefined)
                customNumericLabels.push(
                    label.join(INTRA_BIN_DELIMITER).trim() || undefined
                )
            })
 
        // TODO: once Grammar#parse() is called for all values, we can remove parseFloat() here
        // See issue: https://www.notion.so/owid/ColumnGrammar-parse-function-does-not-get-applied-67b578b8af7642c5859a1db79c8d5712
        const customNumericMinValue = scale.colorScaleNumericMinValue
            ? parseFloat(scale.colorScaleNumericMinValue as any)
            : undefined
 
        const customNumericColorsActive =
            customNumericColors.length > 0 ? true : undefined
 
        const customCategoryColors: {
            [key: string]: string | undefined
        } = {}
        const customCategoryLabels: {
            [key: string]: string | undefined
        } = {}
        scale.colorScaleCategoricalBins
            ?.split(INTER_BIN_DELIMITER)
            .forEach((bin): void => {
                const [value, color, ...label] = bin.split(
                    INTRA_BIN_DELIMITER
                ) as (string | undefined)[]
                if (!value) return
                customCategoryColors[value] = color?.trim() || undefined
                customCategoryLabels[value] =
                    label.join(INTRA_BIN_DELIMITER).trim() || undefined
            })
        if (scale.colorScaleNoDataLabel) {
            customCategoryLabels[NO_DATA_LABEL] = scale.colorScaleNoDataLabel
        }
 
        // Use user-defined binning strategy, otherwise set to manual if user has
        // defined custom bins
        const binningStrategy = scale.colorScaleBinningStrategy
            ? (scale.colorScaleBinningStrategy as BinningStrategy)
            : scale.colorScaleNumericBins || scale.colorScaleCategoricalBins
            ? BinningStrategy.manual
            : undefined
 
        const equalSizeBins = scale.colorScaleEqualSizeBins
 
        const legendDescription = scale.colorScaleLegendDescription
 
        const trimmed: Partial<ColorScaleConfig> = trimObject({
            colorSchemeInvert,
            baseColorScheme,
            binningStrategy,
            customNumericValues,
            customNumericColors,
            customNumericColorsActive,
            customNumericLabels,
            customNumericMinValue,
            customCategoryLabels,
            customCategoryColors,
            equalSizeBins,
            legendDescription,
        })
 
        return isEmpty(trimmed) ? undefined : new ColorScaleConfig(trimmed)
    }
 
    toDSL(): ColumnColorScale {
        const {
            colorSchemeInvert,
            baseColorScheme,
            binningStrategy,
            customNumericValues,
            customNumericColors,
            customNumericLabels,
            customNumericMinValue,
            customCategoryLabels,
            customCategoryColors,
            equalSizeBins,
            legendDescription,
        } = this.toObject()

        const columnColorScale: ColumnColorScale = {
            colorScaleScheme: baseColorScheme,
            colorScaleInvert: colorSchemeInvert,
            colorScaleBinningStrategy: binningStrategy,
            colorScaleEqualSizeBins: equalSizeBins,
            colorScaleLegendDescription: legendDescription,
            colorScaleNumericMinValue: customNumericMinValue,
            colorScaleNumericBins: (customNumericValues ?? [])
                .map((value: any, index: number) =>
                    [
                        value,
                        customNumericColors[index] ?? "",
                        customNumericLabels[index],
                    ].join(INTRA_BIN_DELIMITER)
                )
                .join(INTER_BIN_DELIMITER),
            colorScaleNoDataLabel: customCategoryLabels[NO_DATA_LABEL],
            colorScaleCategoricalBins: Object.keys(customCategoryColors ?? {})
                .map((value) =>
                    [
                        value,
                        customCategoryColors[value],
                        customCategoryLabels[value],
                    ].join(INTRA_BIN_DELIMITER)
                )
                .join(INTER_BIN_DELIMITER),
        }
 
        return trimObject(columnColorScale)
    }
}
 
const INTER_BIN_DELIMITER = ";"
const INTRA_BIN_DELIMITER = ","