| /* |
| * 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 env from 'zrender/src/core/env'; |
| 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 |
| } from '../../util/types'; |
| import ComponentModel from '../../model/Component'; |
| import Model from '../../model/Model'; |
| import GlobalModel from '../../model/Global'; |
| import SeriesModel from '../../model/Series'; |
| import List from '../../data/List'; |
| |
| 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?: number |
| } |
| |
| 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; |
| |
| // FIXME |
| // necessary? |
| // Disable realtime view update if canvas is not supported. |
| if (!env.canvasSupported) { |
| thisOption.realtime = false; |
| } |
| |
| !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 |
| ); |
| } |
| |
| /** |
| * @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; |
| } |
| |
| /** |
| * Return Concrete dimention. If return null/undefined, no dimension used. |
| */ |
| getDataDimension(list: List) { |
| const optDim = this.option.dimension; |
| const listDimensions = list.dimensions; |
| if (optDim == null && !listDimensions.length) { |
| return; |
| } |
| |
| if (optDim != null) { |
| return list.getDimension(optDim); |
| } |
| |
| const dimNames = list.dimensions; |
| for (let i = dimNames.length - 1; i >= 0; i--) { |
| const dimName = dimNames[i]; |
| const dimInfo = list.getDimensionInfo(dimName); |
| if (!dimInfo.isCalculationCoord) { |
| return dimName; |
| } |
| } |
| } |
| |
| 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'); |
| |
| 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 ? 'roundRect' : ['roundRect']); |
| } |
| if (visuals.symbolSize == null) { |
| visuals.symbolSize = symbolSizeExists |
| && zrUtil.clone(symbolSizeExists) |
| || (isCategory ? itemSize[0] : [itemSize[0], itemSize[0]]); |
| } |
| |
| // Filter square and none. |
| visuals.symbol = mapVisual(visuals.symbol, function (symbol) { |
| return (symbol === 'none' || symbol === 'square') ? 'roundRect' : 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; |