|  | /* | 
|  | * 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 visualDefault from '../../visual/visualDefault'; | 
|  | import VisualMapping, { VisualMappingOption } from '../../visual/VisualMapping'; | 
|  | import * as visualSolution from '../../visual/visualSolution'; | 
|  | import * as modelUtil from '../../util/model'; | 
|  | import * as numberUtil from '../../util/number'; | 
|  | import { | 
|  | ComponentOption, | 
|  | BoxLayoutOptionMixin, | 
|  | LabelOption, | 
|  | ColorString, | 
|  | ZRColor, | 
|  | BorderOptionMixin, | 
|  | OptionDataValue, | 
|  | BuiltinVisualProperty, | 
|  | DimensionIndex | 
|  | } from '../../util/types'; | 
|  | import ComponentModel from '../../model/Component'; | 
|  | import Model from '../../model/Model'; | 
|  | import GlobalModel from '../../model/Global'; | 
|  | import SeriesModel from '../../model/Series'; | 
|  | import SeriesData from '../../data/SeriesData'; | 
|  |  | 
|  | const mapVisual = VisualMapping.mapVisual; | 
|  | const eachVisual = VisualMapping.eachVisual; | 
|  | const isArray = zrUtil.isArray; | 
|  | const each = zrUtil.each; | 
|  | const asc = numberUtil.asc; | 
|  | const linearMap = numberUtil.linearMap; | 
|  |  | 
|  | type VisualOptionBase = {[key in BuiltinVisualProperty]?: any}; | 
|  |  | 
|  | type LabelFormatter = (min: OptionDataValue, max?: OptionDataValue) => string; | 
|  |  | 
|  | type VisualState = VisualMapModel['stateList'][number]; | 
|  | export interface VisualMapOption<T extends VisualOptionBase = VisualOptionBase> extends | 
|  | ComponentOption, | 
|  | BoxLayoutOptionMixin, | 
|  | BorderOptionMixin { | 
|  |  | 
|  | mainType?: 'visualMap' | 
|  |  | 
|  | show?: boolean | 
|  |  | 
|  | align?: string | 
|  |  | 
|  | realtime?: boolean | 
|  | /** | 
|  | * 'all' or null/undefined: all series. | 
|  | * A number or an array of number: the specified series. | 
|  | * set min: 0, max: 200, only for campatible with ec2. | 
|  | * In fact min max should not have default value. | 
|  | */ | 
|  | seriesIndex?: 'all' | number[] | number | 
|  |  | 
|  | /** | 
|  | * min value, must specified if pieces is not specified. | 
|  | */ | 
|  | min?: number | 
|  |  | 
|  | /** | 
|  | * max value, must specified if pieces is not specified. | 
|  | */ | 
|  | max?: number | 
|  | /** | 
|  | * Dimension to be encoded | 
|  | */ | 
|  | dimension?: number | 
|  |  | 
|  | /** | 
|  | * Visual configuration for the data in selection | 
|  | */ | 
|  | inRange?: T | 
|  | /** | 
|  | * Visual configuration for the out of selection | 
|  | */ | 
|  | outOfRange?: T | 
|  |  | 
|  | controller?: { | 
|  | inRange?: T | 
|  | outOfRange?: T | 
|  | } | 
|  | target?: { | 
|  | inRange?: T | 
|  | outOfRange?: T | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Width of the display item | 
|  | */ | 
|  | itemWidth?: number | 
|  | /** | 
|  | * Height of the display item | 
|  | */ | 
|  | itemHeight?: number | 
|  |  | 
|  | inverse?: boolean | 
|  |  | 
|  | orient?: 'horizontal' | 'vertical' | 
|  |  | 
|  | backgroundColor?: ZRColor | 
|  | contentColor?: ZRColor | 
|  |  | 
|  | inactiveColor?: ZRColor | 
|  |  | 
|  | /** | 
|  | * Padding of the component. Can be an array similar to CSS | 
|  | */ | 
|  | padding?: number[] | number | 
|  | /** | 
|  | * Gap between text and item | 
|  | */ | 
|  | textGap?: number | 
|  |  | 
|  | precision?: number | 
|  |  | 
|  | /** | 
|  | * @deprecated | 
|  | * Option from version 2 | 
|  | */ | 
|  | color?: ColorString[] | 
|  |  | 
|  | formatter?: string | LabelFormatter | 
|  |  | 
|  | /** | 
|  | * Text on the both end. Such as ['High', 'Low'] | 
|  | */ | 
|  | text?: string[] | 
|  |  | 
|  | textStyle?: LabelOption | 
|  |  | 
|  |  | 
|  | categories?: unknown | 
|  | } | 
|  |  | 
|  | export interface VisualMeta { | 
|  | stops: { value: number, color: ColorString}[] | 
|  | outerColors: ColorString[] | 
|  |  | 
|  | dimension?: DimensionIndex | 
|  | } | 
|  |  | 
|  | class VisualMapModel<Opts extends VisualMapOption = VisualMapOption> extends ComponentModel<Opts> { | 
|  |  | 
|  | static type = 'visualMap'; | 
|  | type = VisualMapModel.type; | 
|  |  | 
|  | static readonly dependencies = ['series']; | 
|  |  | 
|  | readonly stateList = ['inRange', 'outOfRange'] as const; | 
|  |  | 
|  | readonly replacableOptionKeys = [ | 
|  | 'inRange', 'outOfRange', 'target', 'controller', 'color' | 
|  | ] as const; | 
|  |  | 
|  | readonly layoutMode = { | 
|  | type: 'box', ignoreSize: true | 
|  | } as const; | 
|  |  | 
|  | /** | 
|  | * [lowerBound, upperBound] | 
|  | */ | 
|  | dataBound = [-Infinity, Infinity]; | 
|  |  | 
|  | protected _dataExtent: [number, number]; | 
|  |  | 
|  | targetVisuals = {} as ReturnType<typeof visualSolution.createVisualMappings>; | 
|  |  | 
|  | controllerVisuals = {} as ReturnType<typeof visualSolution.createVisualMappings>; | 
|  |  | 
|  | textStyleModel: Model<LabelOption>; | 
|  |  | 
|  | itemSize: number[]; | 
|  |  | 
|  | init(option: Opts, parentModel: Model, ecModel: GlobalModel) { | 
|  | this.mergeDefaultAndTheme(option, ecModel); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @protected | 
|  | */ | 
|  | optionUpdated(newOption: Opts, isInit?: boolean) { | 
|  | const thisOption = this.option; | 
|  |  | 
|  | !isInit && visualSolution.replaceVisualOption( | 
|  | thisOption, newOption, this.replacableOptionKeys | 
|  | ); | 
|  |  | 
|  | this.textStyleModel = this.getModel('textStyle'); | 
|  |  | 
|  | this.resetItemSize(); | 
|  |  | 
|  | this.completeVisualOption(); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @protected | 
|  | */ | 
|  | resetVisual( | 
|  | supplementVisualOption: (this: this, mappingOption: VisualMappingOption, state: string) => void | 
|  | ) { | 
|  | const stateList = this.stateList; | 
|  | supplementVisualOption = zrUtil.bind(supplementVisualOption, this); | 
|  |  | 
|  | this.controllerVisuals = visualSolution.createVisualMappings( | 
|  | this.option.controller, stateList, supplementVisualOption | 
|  | ); | 
|  | this.targetVisuals = visualSolution.createVisualMappings( | 
|  | this.option.target, stateList, supplementVisualOption | 
|  | ); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @public | 
|  | */ | 
|  | getItemSymbol(): string { | 
|  | return null; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @protected | 
|  | * @return {Array.<number>} An array of series indices. | 
|  | */ | 
|  | getTargetSeriesIndices() { | 
|  | const optionSeriesIndex = this.option.seriesIndex; | 
|  | let seriesIndices: number[] = []; | 
|  |  | 
|  | if (optionSeriesIndex == null || optionSeriesIndex === 'all') { | 
|  | this.ecModel.eachSeries(function (seriesModel, index) { | 
|  | seriesIndices.push(index); | 
|  | }); | 
|  | } | 
|  | else { | 
|  | seriesIndices = modelUtil.normalizeToArray(optionSeriesIndex); | 
|  | } | 
|  |  | 
|  | return seriesIndices; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @public | 
|  | */ | 
|  | eachTargetSeries<Ctx>( | 
|  | callback: (this: Ctx, series: SeriesModel) => void, | 
|  | context?: Ctx | 
|  | ) { | 
|  | zrUtil.each(this.getTargetSeriesIndices(), function (seriesIndex) { | 
|  | const seriesModel = this.ecModel.getSeriesByIndex(seriesIndex); | 
|  | if (seriesModel) { | 
|  | callback.call(context, seriesModel); | 
|  | } | 
|  | }, this); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @pubilc | 
|  | */ | 
|  | isTargetSeries(seriesModel: SeriesModel) { | 
|  | let is = false; | 
|  | this.eachTargetSeries(function (model) { | 
|  | model === seriesModel && (is = true); | 
|  | }); | 
|  | return is; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @example | 
|  | * this.formatValueText(someVal); // format single numeric value to text. | 
|  | * this.formatValueText(someVal, true); // format single category value to text. | 
|  | * this.formatValueText([min, max]); // format numeric min-max to text. | 
|  | * this.formatValueText([this.dataBound[0], max]); // using data lower bound. | 
|  | * this.formatValueText([min, this.dataBound[1]]); // using data upper bound. | 
|  | * | 
|  | * @param value Real value, or this.dataBound[0 or 1]. | 
|  | * @param isCategory Only available when value is number. | 
|  | * @param edgeSymbols Open-close symbol when value is interval. | 
|  | * @protected | 
|  | */ | 
|  | formatValueText( | 
|  | value: number | string | number[], | 
|  | isCategory?: boolean, | 
|  | edgeSymbols?: string[] | 
|  | ): string { | 
|  | const option = this.option; | 
|  | const precision = option.precision; | 
|  | const dataBound = this.dataBound; | 
|  | const formatter = option.formatter; | 
|  | let isMinMax: boolean; | 
|  | edgeSymbols = edgeSymbols || ['<', '>'] as [string, string]; | 
|  |  | 
|  | if (zrUtil.isArray(value)) { | 
|  | value = value.slice(); | 
|  | isMinMax = true; | 
|  | } | 
|  |  | 
|  | const textValue = isCategory | 
|  | ? value as string   // Value is string when isCategory | 
|  | : (isMinMax | 
|  | ? [toFixed((value as number[])[0]), toFixed((value as number[])[1])] | 
|  | : toFixed(value as number) | 
|  | ); | 
|  |  | 
|  | if (zrUtil.isString(formatter)) { | 
|  | return formatter | 
|  | .replace('{value}', isMinMax ? (textValue as string[])[0] : textValue as string) | 
|  | .replace('{value2}', isMinMax ? (textValue as string[])[1] : textValue as string); | 
|  | } | 
|  | else if (zrUtil.isFunction(formatter)) { | 
|  | return isMinMax | 
|  | ? formatter((value as number[])[0], (value as number[])[1]) | 
|  | : formatter(value as number); | 
|  | } | 
|  |  | 
|  | if (isMinMax) { | 
|  | if ((value as number[])[0] === dataBound[0]) { | 
|  | return edgeSymbols[0] + ' ' + textValue[1]; | 
|  | } | 
|  | else if ((value as number[])[1] === dataBound[1]) { | 
|  | return edgeSymbols[1] + ' ' + textValue[0]; | 
|  | } | 
|  | else { | 
|  | return textValue[0] + ' - ' + textValue[1]; | 
|  | } | 
|  | } | 
|  | else { // Format single value (includes category case). | 
|  | return textValue as string; | 
|  | } | 
|  |  | 
|  | function toFixed(val: number) { | 
|  | return val === dataBound[0] | 
|  | ? 'min' | 
|  | : val === dataBound[1] | 
|  | ? 'max' | 
|  | : (+val).toFixed(Math.min(precision, 20)); | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @protected | 
|  | */ | 
|  | resetExtent() { | 
|  | const thisOption = this.option; | 
|  |  | 
|  | // Can not calculate data extent by data here. | 
|  | // Because series and data may be modified in processing stage. | 
|  | // So we do not support the feature "auto min/max". | 
|  |  | 
|  | const extent = asc([thisOption.min, thisOption.max] as [number, number]); | 
|  |  | 
|  | this._dataExtent = extent; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * PENDING: | 
|  | * delete this method if no outer usage. | 
|  | * | 
|  | * Return  Concrete dimention. If return null/undefined, no dimension used. | 
|  | */ | 
|  | // getDataDimension(data: SeriesData) { | 
|  | //     const optDim = this.option.dimension; | 
|  |  | 
|  | //     if (optDim != null) { | 
|  | //         return data.getDimension(optDim); | 
|  | //     } | 
|  |  | 
|  | //     const dimNames = data.dimensions; | 
|  | //     for (let i = dimNames.length - 1; i >= 0; i--) { | 
|  | //         const dimName = dimNames[i]; | 
|  | //         const dimInfo = data.getDimensionInfo(dimName); | 
|  | //         if (!dimInfo.isCalculationCoord) { | 
|  | //             return dimName; | 
|  | //         } | 
|  | //     } | 
|  | // } | 
|  |  | 
|  | getDataDimensionIndex(data: SeriesData): DimensionIndex { | 
|  | const optDim = this.option.dimension; | 
|  |  | 
|  | if (optDim != null) { | 
|  | return data.getDimensionIndex(optDim); | 
|  | } | 
|  |  | 
|  | const dimNames = data.dimensions; | 
|  | for (let i = dimNames.length - 1; i >= 0; i--) { | 
|  | const dimName = dimNames[i]; | 
|  | const dimInfo = data.getDimensionInfo(dimName); | 
|  | if (!dimInfo.isCalculationCoord) { | 
|  | return dimInfo.storeDimIndex; | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | getExtent() { | 
|  | return this._dataExtent.slice() as [number, number]; | 
|  | } | 
|  |  | 
|  | completeVisualOption() { | 
|  |  | 
|  | const ecModel = this.ecModel; | 
|  | const thisOption = this.option; | 
|  | const base = { | 
|  | inRange: thisOption.inRange, | 
|  | outOfRange: thisOption.outOfRange | 
|  | }; | 
|  |  | 
|  | const target = thisOption.target || (thisOption.target = {}); | 
|  | const controller = thisOption.controller || (thisOption.controller = {}); | 
|  |  | 
|  | zrUtil.merge(target, base); // Do not override | 
|  | zrUtil.merge(controller, base); // Do not override | 
|  |  | 
|  | const isCategory = this.isCategory(); | 
|  |  | 
|  | completeSingle.call(this, target); | 
|  | completeSingle.call(this, controller); | 
|  | completeInactive.call(this, target, 'inRange', 'outOfRange'); | 
|  | // completeInactive.call(this, target, 'outOfRange', 'inRange'); | 
|  | completeController.call(this, controller); | 
|  |  | 
|  | function completeSingle(this: VisualMapModel, base: VisualMapOption['target']) { | 
|  | // Compatible with ec2 dataRange.color. | 
|  | // The mapping order of dataRange.color is: [high value, ..., low value] | 
|  | // whereas inRange.color and outOfRange.color is [low value, ..., high value] | 
|  | // Notice: ec2 has no inverse. | 
|  | if (isArray(thisOption.color) | 
|  | // If there has been inRange: {symbol: ...}, adding color is a mistake. | 
|  | // So adding color only when no inRange defined. | 
|  | && !base.inRange | 
|  | ) { | 
|  | base.inRange = {color: thisOption.color.slice().reverse()}; | 
|  | } | 
|  |  | 
|  | // Compatible with previous logic, always give a defautl color, otherwise | 
|  | // simple config with no inRange and outOfRange will not work. | 
|  | // Originally we use visualMap.color as the default color, but setOption at | 
|  | // the second time the default color will be erased. So we change to use | 
|  | // constant DEFAULT_COLOR. | 
|  | // If user do not want the default color, set inRange: {color: null}. | 
|  | base.inRange = base.inRange || {color: ecModel.get('gradientColor')}; | 
|  | } | 
|  |  | 
|  | function completeInactive( | 
|  | this: VisualMapModel, | 
|  | base: VisualMapOption['target'], | 
|  | stateExist: VisualState, | 
|  | stateAbsent: VisualState | 
|  | ) { | 
|  | const optExist = base[stateExist]; | 
|  | let optAbsent = base[stateAbsent]; | 
|  |  | 
|  | if (optExist && !optAbsent) { | 
|  | optAbsent = base[stateAbsent] = {}; | 
|  | each(optExist, function (visualData, visualType: BuiltinVisualProperty) { | 
|  | if (!VisualMapping.isValidType(visualType)) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | const defa = visualDefault.get(visualType, 'inactive', isCategory); | 
|  |  | 
|  | if (defa != null) { | 
|  | optAbsent[visualType] = defa; | 
|  |  | 
|  | // Compatibable with ec2: | 
|  | // Only inactive color to rgba(0,0,0,0) can not | 
|  | // make label transparent, so use opacity also. | 
|  | if (visualType === 'color' | 
|  | && !optAbsent.hasOwnProperty('opacity') | 
|  | && !optAbsent.hasOwnProperty('colorAlpha') | 
|  | ) { | 
|  | optAbsent.opacity = [0, 0]; | 
|  | } | 
|  | } | 
|  | }); | 
|  | } | 
|  | } | 
|  |  | 
|  | function completeController(this: VisualMapModel, controller?: VisualMapOption['controller']) { | 
|  | const symbolExists = (controller.inRange || {}).symbol | 
|  | || (controller.outOfRange || {}).symbol; | 
|  | const symbolSizeExists = (controller.inRange || {}).symbolSize | 
|  | || (controller.outOfRange || {}).symbolSize; | 
|  | const inactiveColor = this.get('inactiveColor'); | 
|  | const itemSymbol = this.getItemSymbol(); | 
|  | const defaultSymbol = itemSymbol || 'roundRect'; | 
|  |  | 
|  | each(this.stateList, function (state: VisualState) { | 
|  |  | 
|  | const itemSize = this.itemSize; | 
|  | let visuals = controller[state]; | 
|  |  | 
|  | // Set inactive color for controller if no other color | 
|  | // attr (like colorAlpha) specified. | 
|  | if (!visuals) { | 
|  | visuals = controller[state] = { | 
|  | color: isCategory ? inactiveColor : [inactiveColor] | 
|  | }; | 
|  | } | 
|  |  | 
|  | // Consistent symbol and symbolSize if not specified. | 
|  | if (visuals.symbol == null) { | 
|  | visuals.symbol = symbolExists | 
|  | && zrUtil.clone(symbolExists) | 
|  | || (isCategory ? defaultSymbol : [defaultSymbol]); | 
|  | } | 
|  | if (visuals.symbolSize == null) { | 
|  | visuals.symbolSize = symbolSizeExists | 
|  | && zrUtil.clone(symbolSizeExists) | 
|  | || (isCategory ? itemSize[0] : [itemSize[0], itemSize[0]]); | 
|  | } | 
|  |  | 
|  | // Filter none | 
|  | visuals.symbol = mapVisual(visuals.symbol, function (symbol) { | 
|  | return symbol === 'none' ? defaultSymbol : symbol; | 
|  | }); | 
|  |  | 
|  | // Normalize symbolSize | 
|  | const symbolSize = visuals.symbolSize; | 
|  |  | 
|  | if (symbolSize != null) { | 
|  | let max = -Infinity; | 
|  | // symbolSize can be object when categories defined. | 
|  | eachVisual(symbolSize, function (value) { | 
|  | value > max && (max = value); | 
|  | }); | 
|  | visuals.symbolSize = mapVisual(symbolSize, function (value) { | 
|  | return linearMap(value, [0, max], [0, itemSize[0]], true); | 
|  | }); | 
|  | } | 
|  |  | 
|  | }, this); | 
|  | } | 
|  | } | 
|  |  | 
|  | resetItemSize() { | 
|  | this.itemSize = [ | 
|  | parseFloat(this.get('itemWidth') as unknown as string), | 
|  | parseFloat(this.get('itemHeight') as unknown as string) | 
|  | ]; | 
|  | } | 
|  |  | 
|  | isCategory() { | 
|  | return !!this.option.categories; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @public | 
|  | * @abstract | 
|  | */ | 
|  | setSelected(selected?: any) {} | 
|  |  | 
|  | getSelected(): any { | 
|  | return null; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @public | 
|  | * @abstract | 
|  | */ | 
|  | getValueState(value: any): VisualMapModel['stateList'][number] { | 
|  | return null; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * FIXME | 
|  | * Do not publish to thirt-part-dev temporarily | 
|  | * util the interface is stable. (Should it return | 
|  | * a function but not visual meta?) | 
|  | * | 
|  | * @pubilc | 
|  | * @abstract | 
|  | * @param getColorVisual | 
|  | *        params: value, valueState | 
|  | *        return: color | 
|  | * @return {Object} visualMeta | 
|  | *        should includes {stops, outerColors} | 
|  | *        outerColor means [colorBeyondMinValue, colorBeyondMaxValue] | 
|  | */ | 
|  | getVisualMeta(getColorVisual: (value: number, valueState: VisualState) => string): VisualMeta { | 
|  | return null; | 
|  | } | 
|  |  | 
|  |  | 
|  | static defaultOption: VisualMapOption = { | 
|  | show: true, | 
|  |  | 
|  | // zlevel: 0, | 
|  | z: 4, | 
|  |  | 
|  | seriesIndex: 'all', | 
|  |  | 
|  | min: 0, | 
|  | max: 200, | 
|  |  | 
|  | left: 0, | 
|  | right: null, | 
|  | top: null, | 
|  | bottom: 0, | 
|  |  | 
|  | itemWidth: null, | 
|  | itemHeight: null, | 
|  | inverse: false, | 
|  | orient: 'vertical',        // 'horizontal' ¦ 'vertical' | 
|  |  | 
|  | backgroundColor: 'rgba(0,0,0,0)', | 
|  | borderColor: '#ccc',       // 值域边框颜色 | 
|  | contentColor: '#5793f3', | 
|  | inactiveColor: '#aaa', | 
|  | borderWidth: 0, | 
|  | padding: 5, | 
|  | // 接受数组分别设定上右下左边距,同css | 
|  | textGap: 10,               // | 
|  | precision: 0,              // 小数精度,默认为0,无小数点 | 
|  |  | 
|  | textStyle: { | 
|  | color: '#333'          // 值域文字颜色 | 
|  | } | 
|  | }; | 
|  | } | 
|  |  | 
|  | export default VisualMapModel; |