|  | /* | 
|  | * 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 VisualMapModel, { VisualMapOption, VisualMeta } from './VisualMapModel'; | 
|  | import VisualMapping, { VisualMappingOption } from '../../visual/VisualMapping'; | 
|  | import visualDefault from '../../visual/visualDefault'; | 
|  | import {reformIntervals} from '../../util/number'; | 
|  | import { VisualOptionPiecewise, BuiltinVisualProperty } from '../../util/types'; | 
|  | import { Dictionary } from 'zrender/src/core/types'; | 
|  | import { inheritDefaultOption } from '../../util/component'; | 
|  |  | 
|  |  | 
|  | // TODO: use `relationExpression.ts` instead | 
|  | interface VisualPiece extends VisualOptionPiecewise { | 
|  | min?: number | 
|  | max?: number | 
|  | lt?: number | 
|  | gt?: number | 
|  | lte?: number | 
|  | gte?: number | 
|  | value?: number | 
|  |  | 
|  | label?: string | 
|  | } | 
|  |  | 
|  | type VisualState = VisualMapModel['stateList'][number]; | 
|  |  | 
|  | type InnerVisualPiece = VisualMappingOption['pieceList'][number]; | 
|  |  | 
|  | type GetPieceValueType<T extends InnerVisualPiece> | 
|  | = T extends { interval: InnerVisualPiece['interval'] } ? number : string; | 
|  |  | 
|  | /** | 
|  | * Order Rule: | 
|  | * | 
|  | * option.categories / option.pieces / option.text / option.selected: | 
|  | *     If !option.inverse, | 
|  | *     Order when vertical: ['top', ..., 'bottom']. | 
|  | *     Order when horizontal: ['left', ..., 'right']. | 
|  | *     If option.inverse, the meaning of | 
|  | *     the order should be reversed. | 
|  | * | 
|  | * this._pieceList: | 
|  | *     The order is always [low, ..., high]. | 
|  | * | 
|  | * Mapping from location to low-high: | 
|  | *     If !option.inverse | 
|  | *     When vertical, top is high. | 
|  | *     When horizontal, right is high. | 
|  | *     If option.inverse, reverse. | 
|  | */ | 
|  |  | 
|  | export interface PiecewiseVisualMapOption extends VisualMapOption { | 
|  | align?: 'auto' | 'left' | 'right' | 
|  |  | 
|  | minOpen?: boolean | 
|  | maxOpen?: boolean | 
|  |  | 
|  | /** | 
|  | * When put the controller vertically, it is the length of | 
|  | * horizontal side of each item. Otherwise, vertical side. | 
|  | * When put the controller vertically, it is the length of | 
|  | * vertical side of each item. Otherwise, horizontal side. | 
|  | */ | 
|  | itemWidth?: number | 
|  | itemHeight?: number | 
|  |  | 
|  | itemSymbol?: string | 
|  | pieces?: VisualPiece[] | 
|  |  | 
|  | /** | 
|  | * category names, like: ['some1', 'some2', 'some3']. | 
|  | * Attr min/max are ignored when categories set. See "Order Rule" | 
|  | */ | 
|  | categories?: string[] | 
|  |  | 
|  | /** | 
|  | * If set to 5, auto split five pieces equally. | 
|  | * If set to 0 and component type not set, component type will be | 
|  | * determined as "continuous". (It is less reasonable but for ec2 | 
|  | * compatibility, see echarts/component/visualMap/typeDefaulter) | 
|  | */ | 
|  | splitNumber?: number | 
|  |  | 
|  | /** | 
|  | * Object. If not specified, means selected. When pieces and splitNumber: {'0': true, '5': true} | 
|  | * When categories: {'cate1': false, 'cate3': true} When selected === false, means all unselected. | 
|  | */ | 
|  | selected?: Dictionary<boolean> | 
|  | selectedMode?: 'multiple' | 'single' | boolean | 
|  |  | 
|  | /** | 
|  | * By default, when text is used, label will hide (the logic | 
|  | * is remained for compatibility reason) | 
|  | */ | 
|  | showLabel?: boolean | 
|  |  | 
|  | itemGap?: number | 
|  |  | 
|  | hoverLink?: boolean | 
|  | } | 
|  |  | 
|  | class PiecewiseModel extends VisualMapModel<PiecewiseVisualMapOption> { | 
|  |  | 
|  | static type = 'visualMap.piecewise' as const; | 
|  | type = PiecewiseModel.type; | 
|  |  | 
|  | /** | 
|  | * The order is always [low, ..., high]. | 
|  | * [{text: string, interval: Array.<number>}, ...] | 
|  | */ | 
|  | private _pieceList: InnerVisualPiece[] = []; | 
|  |  | 
|  | private _mode: 'pieces' | 'categories' | 'splitNumber'; | 
|  |  | 
|  | optionUpdated(newOption: PiecewiseVisualMapOption, isInit?: boolean) { | 
|  | super.optionUpdated.apply(this, arguments as any); | 
|  |  | 
|  | this.resetExtent(); | 
|  |  | 
|  | const mode = this._mode = this._determineMode(); | 
|  |  | 
|  | this._pieceList = []; | 
|  | resetMethods[this._mode].call(this, this._pieceList); | 
|  |  | 
|  | this._resetSelected(newOption, isInit); | 
|  |  | 
|  | const categories = this.option.categories; | 
|  |  | 
|  | this.resetVisual(function (mappingOption, state) { | 
|  | if (mode === 'categories') { | 
|  | mappingOption.mappingMethod = 'category'; | 
|  | mappingOption.categories = zrUtil.clone(categories); | 
|  | } | 
|  | else { | 
|  | mappingOption.dataExtent = this.getExtent(); | 
|  | mappingOption.mappingMethod = 'piecewise'; | 
|  | mappingOption.pieceList = zrUtil.map(this._pieceList, function (piece) { | 
|  | piece = zrUtil.clone(piece); | 
|  | if (state !== 'inRange') { | 
|  | // FIXME | 
|  | // outOfRange do not support special visual in pieces. | 
|  | piece.visual = null; | 
|  | } | 
|  | return piece; | 
|  | }); | 
|  | } | 
|  | }); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @protected | 
|  | * @override | 
|  | */ | 
|  | completeVisualOption() { | 
|  | // Consider this case: | 
|  | // visualMap: { | 
|  | //      pieces: [{symbol: 'circle', lt: 0}, {symbol: 'rect', gte: 0}] | 
|  | // } | 
|  | // where no inRange/outOfRange set but only pieces. So we should make | 
|  | // default inRange/outOfRange for this case, otherwise visuals that only | 
|  | // appear in `pieces` will not be taken into account in visual encoding. | 
|  |  | 
|  | const option = this.option; | 
|  | const visualTypesInPieces: {[key in BuiltinVisualProperty]?: 0 | 1} = {}; | 
|  | const visualTypes = VisualMapping.listVisualTypes(); | 
|  | const isCategory = this.isCategory(); | 
|  |  | 
|  | zrUtil.each(option.pieces, function (piece) { | 
|  | zrUtil.each(visualTypes, function (visualType: BuiltinVisualProperty) { | 
|  | if (piece.hasOwnProperty(visualType)) { | 
|  | visualTypesInPieces[visualType] = 1; | 
|  | } | 
|  | }); | 
|  | }); | 
|  |  | 
|  | zrUtil.each(visualTypesInPieces, function (v, visualType: BuiltinVisualProperty) { | 
|  | let exists = false; | 
|  | zrUtil.each(this.stateList, function (state: VisualState) { | 
|  | exists = exists || has(option, state, visualType) | 
|  | || has(option.target, state, visualType); | 
|  | }, this); | 
|  |  | 
|  | !exists && zrUtil.each(this.stateList, function (state: VisualState) { | 
|  | (option[state] || (option[state] = {}))[visualType] = visualDefault.get( | 
|  | visualType, state === 'inRange' ? 'active' : 'inactive', isCategory | 
|  | ); | 
|  | }); | 
|  | }, this); | 
|  |  | 
|  | function has(obj: PiecewiseVisualMapOption['target'], state: VisualState, visualType: BuiltinVisualProperty) { | 
|  | return obj && obj[state] && obj[state].hasOwnProperty(visualType); | 
|  | } | 
|  |  | 
|  | super.completeVisualOption.apply(this, arguments as any); | 
|  | } | 
|  |  | 
|  | private _resetSelected(newOption: PiecewiseVisualMapOption, isInit?: boolean) { | 
|  | const thisOption = this.option; | 
|  | const pieceList = this._pieceList; | 
|  |  | 
|  | // Selected do not merge but all override. | 
|  | const selected = (isInit ? thisOption : newOption).selected || {}; | 
|  | thisOption.selected = selected; | 
|  |  | 
|  | // Consider 'not specified' means true. | 
|  | zrUtil.each(pieceList, function (piece, index) { | 
|  | const key = this.getSelectedMapKey(piece); | 
|  | if (!selected.hasOwnProperty(key)) { | 
|  | selected[key] = true; | 
|  | } | 
|  | }, this); | 
|  |  | 
|  | if (thisOption.selectedMode === 'single') { | 
|  | // Ensure there is only one selected. | 
|  | let hasSel = false; | 
|  |  | 
|  | zrUtil.each(pieceList, function (piece, index) { | 
|  | const key = this.getSelectedMapKey(piece); | 
|  | if (selected[key]) { | 
|  | hasSel | 
|  | ? (selected[key] = false) | 
|  | : (hasSel = true); | 
|  | } | 
|  | }, this); | 
|  | } | 
|  | // thisOption.selectedMode === 'multiple', default: all selected. | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @public | 
|  | */ | 
|  | getItemSymbol(): string { | 
|  | return this.get('itemSymbol'); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @public | 
|  | */ | 
|  | getSelectedMapKey(piece: InnerVisualPiece) { | 
|  | return this._mode === 'categories' | 
|  | ? piece.value + '' : piece.index + ''; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @public | 
|  | */ | 
|  | getPieceList(): InnerVisualPiece[] { | 
|  | return this._pieceList; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @return {string} | 
|  | */ | 
|  | private _determineMode() { | 
|  | const option = this.option; | 
|  |  | 
|  | return option.pieces && option.pieces.length > 0 | 
|  | ? 'pieces' | 
|  | : this.option.categories | 
|  | ? 'categories' | 
|  | : 'splitNumber'; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @override | 
|  | */ | 
|  | setSelected(selected: this['option']['selected']) { | 
|  | this.option.selected = zrUtil.clone(selected); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @override | 
|  | */ | 
|  | getValueState(value: number): VisualState { | 
|  | const index = VisualMapping.findPieceIndex(value, this._pieceList); | 
|  |  | 
|  | return index != null | 
|  | ? (this.option.selected[this.getSelectedMapKey(this._pieceList[index])] | 
|  | ? 'inRange' : 'outOfRange' | 
|  | ) | 
|  | : 'outOfRange'; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @public | 
|  | * @param pieceIndex piece index in visualMapModel.getPieceList() | 
|  | */ | 
|  | findTargetDataIndices(pieceIndex: number) { | 
|  | type DataIndices = { | 
|  | seriesId: string | 
|  | dataIndex: number[] | 
|  | }; | 
|  |  | 
|  | const result: DataIndices[] = []; | 
|  | const pieceList = this._pieceList; | 
|  |  | 
|  | this.eachTargetSeries(function (seriesModel) { | 
|  | const dataIndices: number[] = []; | 
|  | const data = seriesModel.getData(); | 
|  |  | 
|  | data.each(this.getDataDimensionIndex(data), function (value: number, dataIndex: number) { | 
|  | // Should always base on model pieceList, because it is order sensitive. | 
|  | const pIdx = VisualMapping.findPieceIndex(value, pieceList); | 
|  | pIdx === pieceIndex && dataIndices.push(dataIndex); | 
|  | }, this); | 
|  |  | 
|  | result.push({seriesId: seriesModel.id, dataIndex: dataIndices}); | 
|  | }, this); | 
|  |  | 
|  | return result; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @private | 
|  | * @param piece piece.value or piece.interval is required. | 
|  | * @return  Can be Infinity or -Infinity | 
|  | */ | 
|  | getRepresentValue(piece: InnerVisualPiece) { | 
|  | let representValue; | 
|  | if (this.isCategory()) { | 
|  | representValue = piece.value; | 
|  | } | 
|  | else { | 
|  | if (piece.value != null) { | 
|  | representValue = piece.value; | 
|  | } | 
|  | else { | 
|  | const pieceInterval = piece.interval || []; | 
|  | representValue = (pieceInterval[0] === -Infinity && pieceInterval[1] === Infinity) | 
|  | ? 0 | 
|  | : (pieceInterval[0] + pieceInterval[1]) / 2; | 
|  | } | 
|  | } | 
|  |  | 
|  | return representValue; | 
|  | } | 
|  |  | 
|  | getVisualMeta( | 
|  | getColorVisual: (value: number, valueState: VisualState) => string | 
|  | ): VisualMeta { | 
|  | // Do not support category. (category axis is ordinal, numerical) | 
|  | if (this.isCategory()) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | const stops: VisualMeta['stops'] = []; | 
|  | const outerColors: VisualMeta['outerColors'] = ['', '']; | 
|  | const visualMapModel = this; | 
|  |  | 
|  | function setStop(interval: [number, number], valueState?: VisualState) { | 
|  | const representValue = visualMapModel.getRepresentValue({ | 
|  | interval: interval | 
|  | }) as number;// Not category | 
|  | if (!valueState) { | 
|  | valueState = visualMapModel.getValueState(representValue); | 
|  | } | 
|  | const color = getColorVisual(representValue, valueState); | 
|  | if (interval[0] === -Infinity) { | 
|  | outerColors[0] = color; | 
|  | } | 
|  | else if (interval[1] === Infinity) { | 
|  | outerColors[1] = color; | 
|  | } | 
|  | else { | 
|  | stops.push( | 
|  | {value: interval[0], color: color}, | 
|  | {value: interval[1], color: color} | 
|  | ); | 
|  | } | 
|  | } | 
|  |  | 
|  | // Suplement | 
|  | const pieceList = this._pieceList.slice(); | 
|  | if (!pieceList.length) { | 
|  | pieceList.push({interval: [-Infinity, Infinity]}); | 
|  | } | 
|  | else { | 
|  | let edge = pieceList[0].interval[0]; | 
|  | edge !== -Infinity && pieceList.unshift({interval: [-Infinity, edge]}); | 
|  | edge = pieceList[pieceList.length - 1].interval[1]; | 
|  | edge !== Infinity && pieceList.push({interval: [edge, Infinity]}); | 
|  | } | 
|  |  | 
|  | let curr = -Infinity; | 
|  | zrUtil.each(pieceList, function (piece) { | 
|  | const interval = piece.interval; | 
|  | if (interval) { | 
|  | // Fulfill gap. | 
|  | interval[0] > curr && setStop([curr, interval[0]], 'outOfRange'); | 
|  | setStop(interval.slice() as [number, number]); | 
|  | curr = interval[1]; | 
|  | } | 
|  | }, this); | 
|  |  | 
|  | return {stops: stops, outerColors: outerColors}; | 
|  | } | 
|  |  | 
|  |  | 
|  | static defaultOption = inheritDefaultOption(VisualMapModel.defaultOption, { | 
|  | selected: null, | 
|  | minOpen: false,             // Whether include values that smaller than `min`. | 
|  | maxOpen: false,             // Whether include values that bigger than `max`. | 
|  |  | 
|  | align: 'auto',              // 'auto', 'left', 'right' | 
|  | itemWidth: 20, | 
|  |  | 
|  | itemHeight: 14, | 
|  |  | 
|  | itemSymbol: 'roundRect', | 
|  | pieces: null, | 
|  | categories: null, | 
|  | splitNumber: 5, | 
|  | selectedMode: 'multiple',   // Can be 'multiple' or 'single'. | 
|  | itemGap: 10,                // The gap between two items, in px. | 
|  | hoverLink: true             // Enable hover highlight. | 
|  | }) as PiecewiseVisualMapOption; | 
|  |  | 
|  | }; | 
|  |  | 
|  | type ResetMethod = (outPieceList: InnerVisualPiece[]) => void; | 
|  | /** | 
|  | * Key is this._mode | 
|  | * @type {Object} | 
|  | * @this {module:echarts/component/viusalMap/PiecewiseMode} | 
|  | */ | 
|  | const resetMethods: Dictionary<ResetMethod> & ThisType<PiecewiseModel> = { | 
|  |  | 
|  | splitNumber(outPieceList) { | 
|  | const thisOption = this.option; | 
|  | let precision = Math.min(thisOption.precision, 20); | 
|  | const dataExtent = this.getExtent(); | 
|  | let splitNumber = thisOption.splitNumber; | 
|  | splitNumber = Math.max(parseInt(splitNumber as unknown as string, 10), 1); | 
|  | thisOption.splitNumber = splitNumber; | 
|  |  | 
|  | let splitStep = (dataExtent[1] - dataExtent[0]) / splitNumber; | 
|  | // Precision auto-adaption | 
|  | while (+splitStep.toFixed(precision) !== splitStep && precision < 5) { | 
|  | precision++; | 
|  | } | 
|  | thisOption.precision = precision; | 
|  | splitStep = +splitStep.toFixed(precision); | 
|  |  | 
|  | if (thisOption.minOpen) { | 
|  | outPieceList.push({ | 
|  | interval: [-Infinity, dataExtent[0]], | 
|  | close: [0, 0] | 
|  | }); | 
|  | } | 
|  |  | 
|  | for ( | 
|  | let index = 0, curr = dataExtent[0]; | 
|  | index < splitNumber; | 
|  | curr += splitStep, index++ | 
|  | ) { | 
|  | const max = index === splitNumber - 1 ? dataExtent[1] : (curr + splitStep); | 
|  |  | 
|  | outPieceList.push({ | 
|  | interval: [curr, max], | 
|  | close: [1, 1] | 
|  | }); | 
|  | } | 
|  |  | 
|  | if (thisOption.maxOpen) { | 
|  | outPieceList.push({ | 
|  | interval: [dataExtent[1], Infinity], | 
|  | close: [0, 0] | 
|  | }); | 
|  | } | 
|  |  | 
|  | reformIntervals(outPieceList as Required<InnerVisualPiece>[]); | 
|  |  | 
|  | zrUtil.each(outPieceList, function (piece, index) { | 
|  | piece.index = index; | 
|  | piece.text = this.formatValueText(piece.interval); | 
|  | }, this); | 
|  | }, | 
|  |  | 
|  | categories(outPieceList) { | 
|  | const thisOption = this.option; | 
|  | zrUtil.each(thisOption.categories, function (cate) { | 
|  | // FIXME category模式也使用pieceList,但在visualMapping中不是使用pieceList。 | 
|  | // 是否改一致。 | 
|  | outPieceList.push({ | 
|  | text: this.formatValueText(cate, true), | 
|  | value: cate | 
|  | }); | 
|  | }, this); | 
|  |  | 
|  | // See "Order Rule". | 
|  | normalizeReverse(thisOption, outPieceList); | 
|  | }, | 
|  |  | 
|  | pieces(outPieceList) { | 
|  | const thisOption = this.option; | 
|  |  | 
|  | zrUtil.each(thisOption.pieces, function (pieceListItem, index) { | 
|  |  | 
|  | if (!zrUtil.isObject(pieceListItem)) { | 
|  | pieceListItem = {value: pieceListItem}; | 
|  | } | 
|  |  | 
|  | const item: InnerVisualPiece = {text: '', index: index}; | 
|  |  | 
|  | if (pieceListItem.label != null) { | 
|  | item.text = pieceListItem.label; | 
|  | } | 
|  |  | 
|  | if (pieceListItem.hasOwnProperty('value')) { | 
|  | const value = item.value = pieceListItem.value; | 
|  | item.interval = [value, value]; | 
|  | item.close = [1, 1]; | 
|  | } | 
|  | else { | 
|  | // `min` `max` is legacy option. | 
|  | // `lt` `gt` `lte` `gte` is recommanded. | 
|  | const interval = item.interval = [] as unknown as [number, number]; | 
|  | const close: typeof item.close = item.close = [0, 0]; | 
|  |  | 
|  | const closeList = [1, 0, 1] as const; | 
|  | const infinityList = [-Infinity, Infinity]; | 
|  |  | 
|  | const useMinMax = []; | 
|  | for (let lg = 0; lg < 2; lg++) { | 
|  | const names = ([['gte', 'gt', 'min'], ['lte', 'lt', 'max']] as const)[lg]; | 
|  | for (let i = 0; i < 3 && interval[lg] == null; i++) { | 
|  | interval[lg] = pieceListItem[names[i]]; | 
|  | close[lg] = closeList[i]; | 
|  | useMinMax[lg] = i === 2; | 
|  | } | 
|  | interval[lg] == null && (interval[lg] = infinityList[lg]); | 
|  | } | 
|  | useMinMax[0] && interval[1] === Infinity && (close[0] = 0); | 
|  | useMinMax[1] && interval[0] === -Infinity && (close[1] = 0); | 
|  |  | 
|  | if (__DEV__) { | 
|  | if (interval[0] > interval[1]) { | 
|  | console.warn( | 
|  | 'Piece ' + index + 'is illegal: ' + interval | 
|  | + ' lower bound should not greater then uppper bound.' | 
|  | ); | 
|  | } | 
|  | } | 
|  |  | 
|  | if (interval[0] === interval[1] && close[0] && close[1]) { | 
|  | // Consider: [{min: 5, max: 5, visual: {...}}, {min: 0, max: 5}], | 
|  | // we use value to lift the priority when min === max | 
|  | item.value = interval[0]; | 
|  | } | 
|  | } | 
|  |  | 
|  | item.visual = VisualMapping.retrieveVisuals(pieceListItem); | 
|  |  | 
|  | outPieceList.push(item); | 
|  |  | 
|  | }, this); | 
|  |  | 
|  | // See "Order Rule". | 
|  | normalizeReverse(thisOption, outPieceList); | 
|  | // Only pieces | 
|  | reformIntervals(outPieceList as Required<InnerVisualPiece>[]); | 
|  |  | 
|  | zrUtil.each(outPieceList, function (piece) { | 
|  | const close = piece.close; | 
|  | const edgeSymbols = [['<', '≤'][close[1]], ['>', '≥'][close[0]]]; | 
|  | piece.text = piece.text || this.formatValueText( | 
|  | piece.value != null ? piece.value : piece.interval, | 
|  | false, | 
|  | edgeSymbols | 
|  | ); | 
|  | }, this); | 
|  | } | 
|  | }; | 
|  |  | 
|  | function normalizeReverse(thisOption: PiecewiseVisualMapOption, pieceList: InnerVisualPiece[]) { | 
|  | const inverse = thisOption.inverse; | 
|  | if (thisOption.orient === 'vertical' ? !inverse : inverse) { | 
|  | pieceList.reverse(); | 
|  | } | 
|  | } | 
|  |  | 
|  | export default PiecewiseModel; |