echarts VisualMapping 源码

  • 2022-10-20
  • 浏览 (318)

echarts VisualMapping 代码

文件路径:/src/visual/VisualMapping.ts

/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements.  See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership.  The ASF licenses this file
* to you 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 * as zrUtil from 'zrender/src/core/util';
import * as zrColor from 'zrender/src/tool/color';
import {linearMap} from '../util/number';
import { AllPropTypes, Dictionary } from 'zrender/src/core/types';
import {
    ColorString,
    BuiltinVisualProperty,
    VisualOptionPiecewise,
    VisualOptionUnit,
    ParsedValue
} from '../util/types';
import { warn } from '../util/log';

const each = zrUtil.each;
const isObject = zrUtil.isObject;

const CATEGORY_DEFAULT_VISUAL_INDEX = -1;

// Type of raw value
type RawValue = ParsedValue;
// Type of mapping visual value
type VisualValue = AllPropTypes<VisualOptionUnit>;
// Type of value after normalized. 0 - 1
type NormalizedValue = number;

type MappingMethod = 'linear' | 'piecewise' | 'category' | 'fixed';

// May include liftZ. which is not provided to developers.

interface Normalizer {
    (this: VisualMapping, value?: RawValue): NormalizedValue
}
interface ColorMapper {
    (this: VisualMapping, value: RawValue | NormalizedValue, isNormalized?: boolean, out?: number[])
        : ColorString | number[]
}
interface DoMap {
    (this: VisualMapping, normalzied?: NormalizedValue, value?: RawValue): VisualValue
}
interface VisualValueGetter {
    (key: string): VisualValue
}
interface VisualValueSetter {
    (key: string, value: VisualValue): void
}
interface VisualHandler {
    applyVisual(
        this: VisualMapping,
        value: RawValue,
        getter: VisualValueGetter,
        setter: VisualValueSetter
    ): void

    _normalizedToVisual: {
        linear(this: VisualMapping, normalized: NormalizedValue): VisualValue
        category(this: VisualMapping, normalized: NormalizedValue): VisualValue
        piecewise(this: VisualMapping, normalzied: NormalizedValue, value: RawValue): VisualValue
        fixed(this: VisualMapping): VisualValue
    }
    /**
     * Get color mapping for the outside usage.
     * Currently only used in `color` visual.
     *
     * The last parameter out is cached color array.
     */
    getColorMapper?: (this: VisualMapping) => ColorMapper
}

interface VisualMappingPiece {
    index?: number

    value?: number | string
    interval?: [number, number]
    close?: [0 | 1, 0 | 1]

    text?: string

    visual?: VisualOptionPiecewise
}

export interface VisualMappingOption {
    type?: BuiltinVisualProperty

    mappingMethod?: MappingMethod

    /**
     * required when mappingMethod is 'linear'
     */
    dataExtent?: [number, number]
    /**
     *  required when mappingMethod is 'piecewise'.
     *  Visual for only each piece can be specified
     * [
     *   {value: someValue},
     *   {interval: [min1, max1], visual: {...}},
     *   {interval: [min2, max2]}
     *  ],.
     */
    pieceList?: VisualMappingPiece[]
    /**
     * required when mappingMethod is 'category'. If no option.categories, categories is set as [0, 1, 2, ...].
     */
    categories?: (string | number)[]
    /**
     * Whether loop mapping when mappingMethod is 'category'.
     * @default false
     */
    loop?: boolean
    /**
     * Visual data
     * when mappingMethod is 'category', visual data can be array or object
     * (like: {cate1: '#222', none: '#fff'})
     * or primary types (which represents default category visual), otherwise visual
     * can be array or primary (which will be normalized to array).
     */
    visual?: VisualValue[] | Dictionary<VisualValue> | VisualValue
}

interface VisualMappingInnerPiece extends VisualMappingPiece {
    originIndex: number
}
interface VisualMappingInnerOption extends VisualMappingOption {
    hasSpecialVisual: boolean
    pieceList: VisualMappingInnerPiece[]
    /**
     * Map to get category index
     */
    categoryMap: Dictionary<number>
    /**
     * Cached parsed rgba array from string to avoid parse every time.
     */
    parsedVisual: number[][]

    // Have converted primary value to array.
    visual?: VisualValue[] | Dictionary<VisualValue>
}

class VisualMapping {

    option: VisualMappingInnerOption;

    type: BuiltinVisualProperty;

    mappingMethod: MappingMethod;

    applyVisual: VisualHandler['applyVisual'];

    getColorMapper: VisualHandler['getColorMapper'];

    _normalizeData: Normalizer;

    _normalizedToVisual: DoMap;

    constructor(option: VisualMappingOption) {
        const mappingMethod = option.mappingMethod;
        const visualType = option.type;

        const thisOption: VisualMappingInnerOption = this.option = zrUtil.clone(option) as VisualMappingInnerOption;

        this.type = visualType;
        this.mappingMethod = mappingMethod;

        this._normalizeData = normalizers[mappingMethod];
        const visualHandler = VisualMapping.visualHandlers[visualType];

        this.applyVisual = visualHandler.applyVisual;

        this.getColorMapper = visualHandler.getColorMapper;

        this._normalizedToVisual = visualHandler._normalizedToVisual[mappingMethod];

        if (mappingMethod === 'piecewise') {
            normalizeVisualRange(thisOption);
            preprocessForPiecewise(thisOption);
        }
        else if (mappingMethod === 'category') {
            thisOption.categories
                ? preprocessForSpecifiedCategory(thisOption)
                // categories is ordinal when thisOption.categories not specified,
                // which need no more preprocess except normalize visual.
                : normalizeVisualRange(thisOption, true);
        }
        else { // mappingMethod === 'linear' or 'fixed'
            zrUtil.assert(mappingMethod !== 'linear' || thisOption.dataExtent);
            normalizeVisualRange(thisOption);
        }
    }

    mapValueToVisual(value: RawValue): VisualValue {
        const normalized = this._normalizeData(value);
        return this._normalizedToVisual(normalized, value);
    }

    getNormalizer() {
        return zrUtil.bind(this._normalizeData, this);
    }

    static visualHandlers: {[key in BuiltinVisualProperty]: VisualHandler} = {
        color: {
            applyVisual: makeApplyVisual('color'),
            getColorMapper: function () {
                const thisOption = this.option;

                return zrUtil.bind(
                    thisOption.mappingMethod === 'category'
                        ? function (
                            this: VisualMapping,
                            value: NormalizedValue | RawValue,
                            isNormalized?: boolean
                        ): ColorString {
                            !isNormalized && (value = this._normalizeData(value));
                            return doMapCategory.call(this, value) as ColorString;
                        }
                        : function (
                            this: VisualMapping,
                            value: NormalizedValue | RawValue,
                            isNormalized?: boolean,
                            out?: number[]
                        ): number[] | string {
                            // If output rgb array
                            // which will be much faster and useful in pixel manipulation
                            const returnRGBArray = !!out;
                            !isNormalized && (value = this._normalizeData(value));
                            out = zrColor.fastLerp(value as NormalizedValue, thisOption.parsedVisual, out);
                            return returnRGBArray ? out : zrColor.stringify(out, 'rgba');
                        },
                    this
                );
            },

            _normalizedToVisual: {
                linear: function (normalized) {
                    return zrColor.stringify(
                        zrColor.fastLerp(normalized, this.option.parsedVisual),
                        'rgba'
                    );
                },
                category: doMapCategory,
                piecewise: function (normalized, value) {
                    let result = getSpecifiedVisual.call(this, value);
                    if (result == null) {
                        result = zrColor.stringify(
                            zrColor.fastLerp(normalized, this.option.parsedVisual),
                            'rgba'
                        );
                    }
                    return result;
                },
                fixed: doMapFixed
            }
        },

        colorHue: makePartialColorVisualHandler(function (color: ColorString, value: number) {
            return zrColor.modifyHSL(color, value);
        }),

        colorSaturation: makePartialColorVisualHandler(function (color: ColorString, value: number) {
            return zrColor.modifyHSL(color, null, value);
        }),

        colorLightness: makePartialColorVisualHandler(function (color: ColorString, value: number) {
            return zrColor.modifyHSL(color, null, null, value);
        }),

        colorAlpha: makePartialColorVisualHandler(function (color: ColorString, value: number) {
            return zrColor.modifyAlpha(color, value);
        }),

        decal: {
            applyVisual: makeApplyVisual('decal'),
            _normalizedToVisual: {
                linear: null,
                category: doMapCategory,
                piecewise: null,
                fixed: null
            }
        },

        opacity: {
            applyVisual: makeApplyVisual('opacity'),
            _normalizedToVisual: createNormalizedToNumericVisual([0, 1])
        },

        liftZ: {
            applyVisual: makeApplyVisual('liftZ'),
            _normalizedToVisual: {
                linear: doMapFixed,
                category: doMapFixed,
                piecewise: doMapFixed,
                fixed: doMapFixed
            }
        },

        symbol: {
            applyVisual: function (value, getter, setter) {
                const symbolCfg = this.mapValueToVisual(value);
                setter('symbol', symbolCfg as string);
            },
            _normalizedToVisual: {
                linear: doMapToArray,
                category: doMapCategory,
                piecewise: function (normalized, value) {
                    let result = getSpecifiedVisual.call(this, value);
                    if (result == null) {
                        result = doMapToArray.call(this, normalized);
                    }
                    return result;
                },
                fixed: doMapFixed
            }
        },

        symbolSize: {
            applyVisual: makeApplyVisual('symbolSize'),
            _normalizedToVisual: createNormalizedToNumericVisual([0, 1])
        }
    };


    /**
     * List available visual types.
     *
     * @public
     * @return {Array.<string>}
     */
    static listVisualTypes() {
        return zrUtil.keys(VisualMapping.visualHandlers);
    }

    // /**
    //  * @public
    //  */
    // static addVisualHandler(name, handler) {
    //     visualHandlers[name] = handler;
    // }

    /**
     * @public
     */
    static isValidType(visualType: string): boolean {
        return VisualMapping.visualHandlers.hasOwnProperty(visualType);
    }

    /**
     * Convenient method.
     * Visual can be Object or Array or primary type.
     */
    static eachVisual<Ctx, T>(
        visual: T | T[] | Dictionary<T>,
        callback: (visual: T, key?: string | number) => void,
        context?: Ctx
    ) {
        if (zrUtil.isObject(visual)) {
            zrUtil.each(visual as Dictionary<T>, callback, context);
        }
        else {
            callback.call(context, visual);
        }
    }

    static mapVisual<Ctx, T>(visual: T, callback: (visual: T, key?: string | number) => T, context?: Ctx): T;
    static mapVisual<Ctx, T>(visual: T[], callback: (visual: T, key?: string | number) => T[], context?: Ctx): T[];
    static mapVisual<Ctx, T>(
        visual: Dictionary<T>,
        callback: (visual: T, key?: string | number) => Dictionary<T>,
        context?: Ctx
    ): Dictionary<T>;
    static mapVisual<Ctx, T>(
        visual: T | T[] | Dictionary<T>,
        callback: (visual: T, key?: string | number) => T | T[] | Dictionary<T>,
        context?: Ctx
    ) {
        let isPrimary: boolean;
        let newVisual: T | T[] | Dictionary<T> = zrUtil.isArray(visual)
            ? []
            : zrUtil.isObject(visual)
            ? {}
            : (isPrimary = true, null);

        VisualMapping.eachVisual(visual, function (v, key) {
            const newVal = callback.call(context, v, key);
            isPrimary ? (newVisual = newVal) : ((newVisual as Dictionary<T>)[key as string] = newVal as T);
        });
        return newVisual;
    }

    /**
     * Retrieve visual properties from given object.
     */
    static retrieveVisuals(obj: Dictionary<any>): VisualOptionPiecewise {
        const ret: VisualOptionPiecewise = {};
        let hasVisual: boolean;

        obj && each(VisualMapping.visualHandlers, function (h, visualType: BuiltinVisualProperty) {
            if (obj.hasOwnProperty(visualType)) {
                (ret as any)[visualType] = obj[visualType];
                hasVisual = true;
            }
        });

        return hasVisual ? ret : null;
    }

    /**
     * Give order to visual types, considering colorSaturation, colorAlpha depends on color.
     *
     * @public
     * @param {(Object|Array)} visualTypes If Object, like: {color: ..., colorSaturation: ...}
     *                                     IF Array, like: ['color', 'symbol', 'colorSaturation']
     * @return {Array.<string>} Sorted visual types.
     */
    static prepareVisualTypes(
        visualTypes: {[key in BuiltinVisualProperty]?: any} | BuiltinVisualProperty[]
    ) {
        if (zrUtil.isArray(visualTypes)) {
            visualTypes = visualTypes.slice();
        }
        else if (isObject(visualTypes)) {
            const types: BuiltinVisualProperty[] = [];
            each(visualTypes, function (item: unknown, type: BuiltinVisualProperty) {
                types.push(type);
            });
            visualTypes = types;
        }
        else {
            return [];
        }

        visualTypes.sort(function (type1: BuiltinVisualProperty, type2: BuiltinVisualProperty) {
            // color should be front of colorSaturation, colorAlpha, ...
            // symbol and symbolSize do not matter.
            return (type2 === 'color' && type1 !== 'color' && type1.indexOf('color') === 0)
                ? 1 : -1;
        });

        return visualTypes;
    }

    /**
     * 'color', 'colorSaturation', 'colorAlpha', ... are depends on 'color'.
     * Other visuals are only depends on themself.
     */
    static dependsOn(visualType1: BuiltinVisualProperty, visualType2: BuiltinVisualProperty) {
        return visualType2 === 'color'
            ? !!(visualType1 && visualType1.indexOf(visualType2) === 0)
            : visualType1 === visualType2;
    }

    /**
     * @param value
     * @param pieceList [{value: ..., interval: [min, max]}, ...]
     *                         Always from small to big.
     * @param findClosestWhenOutside Default to be false
     * @return index
     */
    static findPieceIndex(value: number, pieceList: VisualMappingPiece[], findClosestWhenOutside?: boolean): number {
        let possibleI: number;
        let abs = Infinity;

        // value has the higher priority.
        for (let i = 0, len = pieceList.length; i < len; i++) {
            const pieceValue = pieceList[i].value;
            if (pieceValue != null) {
                if (pieceValue === value
                    // FIXME
                    // It is supposed to compare value according to value type of dimension,
                    // but currently value type can exactly be string or number.
                    // Compromise for numeric-like string (like '12'), especially
                    // in the case that visualMap.categories is ['22', '33'].
                    || (zrUtil.isString(pieceValue) && pieceValue === value + '')
                ) {
                    return i;
                }
                findClosestWhenOutside && updatePossible(pieceValue as number, i);
            }
        }

        for (let i = 0, len = pieceList.length; i < len; i++) {
            const piece = pieceList[i];
            const interval = piece.interval;
            const close = piece.close;

            if (interval) {
                if (interval[0] === -Infinity) {
                    if (littleThan(close[1], value, interval[1])) {
                        return i;
                    }
                }
                else if (interval[1] === Infinity) {
                    if (littleThan(close[0], interval[0], value)) {
                        return i;
                    }
                }
                else if (
                    littleThan(close[0], interval[0], value)
                    && littleThan(close[1], value, interval[1])
                ) {
                    return i;
                }
                findClosestWhenOutside && updatePossible(interval[0], i);
                findClosestWhenOutside && updatePossible(interval[1], i);
            }
        }

        if (findClosestWhenOutside) {
            return value === Infinity
                ? pieceList.length - 1
                : value === -Infinity
                ? 0
                : possibleI;
        }

        function updatePossible(val: number, index: number) {
            const newAbs = Math.abs(val - value);
            if (newAbs < abs) {
                abs = newAbs;
                possibleI = index;
            }
        }

    }
}

function preprocessForPiecewise(thisOption: VisualMappingInnerOption) {
    const pieceList = thisOption.pieceList;
    thisOption.hasSpecialVisual = false;

    zrUtil.each(pieceList, function (piece, index) {
        piece.originIndex = index;
        // piece.visual is "result visual value" but not
        // a visual range, so it does not need to be normalized.
        if (piece.visual != null) {
            thisOption.hasSpecialVisual = true;
        }
    });
}

function preprocessForSpecifiedCategory(thisOption: VisualMappingInnerOption) {
    // Hash categories.
    const categories = thisOption.categories;
    const categoryMap: VisualMappingInnerOption['categoryMap'] = thisOption.categoryMap = {};

    let visual = thisOption.visual;
    each(categories, function (cate, index) {
        categoryMap[cate] = index;
    });

    // Process visual map input.
    if (!zrUtil.isArray(visual)) {
        const visualArr: VisualValue[] = [];

        if (zrUtil.isObject(visual)) {
            each(visual, function (v, cate) {
                const index = categoryMap[cate];
                visualArr[index != null ? index : CATEGORY_DEFAULT_VISUAL_INDEX] = v;
            });
        }
        else { // Is primary type, represents default visual.
            visualArr[CATEGORY_DEFAULT_VISUAL_INDEX] = visual;
        }

        visual = setVisualToOption(thisOption, visualArr);
    }

    // Remove categories that has no visual,
    // then we can mapping them to CATEGORY_DEFAULT_VISUAL_INDEX.
    for (let i = categories.length - 1; i >= 0; i--) {
        if (visual[i] == null) {
            delete categoryMap[categories[i]];
            categories.pop();
        }
    }
}

function normalizeVisualRange(thisOption: VisualMappingInnerOption, isCategory?: boolean) {
    const visual = thisOption.visual;
    const visualArr: VisualValue[] = [];

    if (zrUtil.isObject(visual)) {
        each(visual, function (v) {
            visualArr.push(v);
        });
    }
    else if (visual != null) {
        visualArr.push(visual);
    }

    const doNotNeedPair = {color: 1, symbol: 1};

    if (!isCategory
        && visualArr.length === 1
        && !doNotNeedPair.hasOwnProperty(thisOption.type)
    ) {
        // Do not care visualArr.length === 0, which is illegal.
        visualArr[1] = visualArr[0];
    }

    setVisualToOption(thisOption, visualArr);
}

function makePartialColorVisualHandler(
    applyValue: (prop: VisualValue, value: NormalizedValue) => VisualValue
): VisualHandler {
    return {
        applyVisual: function (value, getter, setter) {
            // Only used in HSL
            const colorChannel = this.mapValueToVisual(value);
            // Must not be array value
            setter('color', applyValue(getter('color'), colorChannel as number));
        },
        _normalizedToVisual: createNormalizedToNumericVisual([0, 1])
    };
}

function doMapToArray(this: VisualMapping, normalized: NormalizedValue): VisualValue {
    const visual = this.option.visual as VisualValue[];
    return visual[
        Math.round(linearMap(normalized, [0, 1], [0, visual.length - 1], true))
    ] || {} as any;    // TODO {}?
}

function makeApplyVisual(visualType: string): VisualHandler['applyVisual'] {
    return function (value, getter, setter) {
        setter(visualType, this.mapValueToVisual(value));
    };
}

function doMapCategory(this: VisualMapping, normalized: NormalizedValue): VisualValue {
    const visual = this.option.visual as Dictionary<any>;
    return visual[
        (this.option.loop && normalized !== CATEGORY_DEFAULT_VISUAL_INDEX)
            ? normalized % visual.length
            : normalized
    ];
}

function doMapFixed(this: VisualMapping): VisualValue {
    // visual will be convert to array.
    return (this.option.visual as VisualValue[])[0];
}

/**
 * Create mapped to numeric visual
 */
function createNormalizedToNumericVisual(sourceExtent: [number, number]): VisualHandler['_normalizedToVisual'] {
    return {
        linear: function (normalized) {
            return linearMap(normalized, sourceExtent, this.option.visual as [number, number], true);
        },
        category: doMapCategory,
        piecewise: function (normalized, value) {
            let result = getSpecifiedVisual.call(this, value);
            if (result == null) {
                result = linearMap(normalized, sourceExtent, this.option.visual as [number, number], true);
            }
            return result;
        },
        fixed: doMapFixed
    };
}

function getSpecifiedVisual(this: VisualMapping, value: number) {
    const thisOption = this.option;
    const pieceList = thisOption.pieceList;
    if (thisOption.hasSpecialVisual) {
        const pieceIndex = VisualMapping.findPieceIndex(value, pieceList);
        const piece = pieceList[pieceIndex];
        if (piece && piece.visual) {
            return piece.visual[this.type];
        }
    }
}

function setVisualToOption(thisOption: VisualMappingInnerOption, visualArr: VisualValue[]) {
    thisOption.visual = visualArr;
    if (thisOption.type === 'color') {
        thisOption.parsedVisual = zrUtil.map(visualArr, function (item: string) {
            const color = zrColor.parse(item);
            if (!color && __DEV__) {
                warn(`'${item}' is an illegal color, fallback to '#000000'`, true);
            }
            return color || [0, 0, 0, 1];
        });
    }
    return visualArr;
}


/**
 * Normalizers by mapping methods.
 */
const normalizers: { [key in MappingMethod]: Normalizer } = {
    linear: function (value: RawValue): NormalizedValue {
        return linearMap(value as number, this.option.dataExtent, [0, 1], true);
    },

    piecewise: function (value: RawValue): NormalizedValue {
        const pieceList = this.option.pieceList;
        const pieceIndex = VisualMapping.findPieceIndex(value as number, pieceList, true);
        if (pieceIndex != null) {
            return linearMap(pieceIndex, [0, pieceList.length - 1], [0, 1], true);
        }
    },

    category: function (value: RawValue): NormalizedValue {
        const index: number = this.option.categories
            ? this.option.categoryMap[value]
            : value as number; // ordinal value
        return index == null ? CATEGORY_DEFAULT_VISUAL_INDEX : index;
    },

    fixed: zrUtil.noop as Normalizer
};


function littleThan(close: boolean | 0 | 1, a: number, b: number): boolean {
    return close ? a <= b : a < b;
}

export default VisualMapping;

相关信息

echarts 源码目录

相关文章

echarts LegendVisualProvider 源码

echarts aria 源码

echarts commonVisualTypes 源码

echarts decal 源码

echarts helper 源码

echarts style 源码

echarts symbol 源码

echarts visualDefault 源码

echarts visualSolution 源码

0  赞