|  | /* | 
|  | * 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 LinearGradient, { LinearGradientObject } from 'zrender/src/graphic/LinearGradient'; | 
|  | import * as eventTool from 'zrender/src/core/event'; | 
|  | import VisualMapView from './VisualMapView'; | 
|  | import * as graphic from '../../util/graphic'; | 
|  | import * as numberUtil from '../../util/number'; | 
|  | import sliderMove from '../helper/sliderMove'; | 
|  | import * as helper from './helper'; | 
|  | import * as modelUtil from '../../util/model'; | 
|  | import VisualMapModel from './VisualMapModel'; | 
|  | import ContinuousModel from './ContinuousModel'; | 
|  | import GlobalModel from '../../model/Global'; | 
|  | import ExtensionAPI from '../../core/ExtensionAPI'; | 
|  | import Element, { ElementEvent } from 'zrender/src/Element'; | 
|  | import { TextVerticalAlign, TextAlign } from 'zrender/src/core/types'; | 
|  | import { ColorString, Payload } from '../../util/types'; | 
|  | import { parsePercent } from 'zrender/src/contain/text'; | 
|  | import { setAsHighDownDispatcher } from '../../util/states'; | 
|  | import { createSymbol } from '../../util/symbol'; | 
|  | import ZRImage from 'zrender/src/graphic/Image'; | 
|  | import { ECData, getECData } from '../../util/innerStore'; | 
|  | import { createTextStyle } from '../../label/labelStyle'; | 
|  | import { findEventDispatcher } from '../../util/event'; | 
|  |  | 
|  | const linearMap = numberUtil.linearMap; | 
|  | const each = zrUtil.each; | 
|  | const mathMin = Math.min; | 
|  | const mathMax = Math.max; | 
|  |  | 
|  | // Arbitrary value | 
|  | const HOVER_LINK_SIZE = 12; | 
|  | const HOVER_LINK_OUT = 6; | 
|  |  | 
|  | type Orient = VisualMapModel['option']['orient']; | 
|  |  | 
|  | type ShapeStorage = { | 
|  | handleThumbs: graphic.Path[] | 
|  | handleLabelPoints: number[][] | 
|  | handleLabels: graphic.Text[] | 
|  |  | 
|  | inRange: graphic.Polygon | 
|  | outOfRange: graphic.Polygon | 
|  |  | 
|  | mainGroup: graphic.Group | 
|  |  | 
|  | indicator: graphic.Path | 
|  | indicatorLabel: graphic.Text | 
|  | indicatorLabelPoint: number[] | 
|  | }; | 
|  |  | 
|  | type TargetDataIndices = ReturnType<ContinuousModel['findTargetDataIndices']>; | 
|  |  | 
|  | type BarVisual = { | 
|  | barColor: LinearGradient, | 
|  | barPoints: number[][] | 
|  | handlesColor: ColorString[] | 
|  | }; | 
|  |  | 
|  | type Direction = 'left' | 'right' | 'top' | 'bottom'; | 
|  | // Notice: | 
|  | // Any "interval" should be by the order of [low, high]. | 
|  | // "handle0" (handleIndex === 0) maps to | 
|  | // low data value: this._dataInterval[0] and has low coord. | 
|  | // "handle1" (handleIndex === 1) maps to | 
|  | // high data value: this._dataInterval[1] and has high coord. | 
|  | // The logic of transform is implemented in this._createBarGroup. | 
|  |  | 
|  | class ContinuousView extends VisualMapView { | 
|  | static type = 'visualMap.continuous'; | 
|  | type = ContinuousView.type; | 
|  |  | 
|  | visualMapModel: ContinuousModel; | 
|  |  | 
|  | private _shapes = {} as ShapeStorage; | 
|  |  | 
|  | private _dataInterval: number[] = []; | 
|  |  | 
|  | private _handleEnds: number[] = []; | 
|  |  | 
|  | private _orient: Orient; | 
|  |  | 
|  | private _useHandle: boolean; | 
|  |  | 
|  | private _hoverLinkDataIndices: TargetDataIndices = []; | 
|  |  | 
|  | private _dragging: boolean; | 
|  |  | 
|  | private _hovering: boolean; | 
|  |  | 
|  | private _firstShowIndicator: boolean; | 
|  |  | 
|  | private _api: ExtensionAPI; | 
|  |  | 
|  |  | 
|  | doRender( | 
|  | visualMapModel: ContinuousModel, | 
|  | ecModel: GlobalModel, | 
|  | api: ExtensionAPI, | 
|  | payload: {type: string, from: string} | 
|  | ) { | 
|  | this._api = api; | 
|  |  | 
|  | if (!payload || payload.type !== 'selectDataRange' || payload.from !== this.uid) { | 
|  | this._buildView(); | 
|  | } | 
|  | } | 
|  |  | 
|  | private _buildView() { | 
|  | this.group.removeAll(); | 
|  |  | 
|  | const visualMapModel = this.visualMapModel; | 
|  | const thisGroup = this.group; | 
|  |  | 
|  | this._orient = visualMapModel.get('orient'); | 
|  | this._useHandle = visualMapModel.get('calculable'); | 
|  |  | 
|  | this._resetInterval(); | 
|  |  | 
|  | this._renderBar(thisGroup); | 
|  |  | 
|  | const dataRangeText = visualMapModel.get('text'); | 
|  | this._renderEndsText(thisGroup, dataRangeText, 0); | 
|  | this._renderEndsText(thisGroup, dataRangeText, 1); | 
|  |  | 
|  | // Do this for background size calculation. | 
|  | this._updateView(true); | 
|  |  | 
|  | // After updating view, inner shapes is built completely, | 
|  | // and then background can be rendered. | 
|  | this.renderBackground(thisGroup); | 
|  |  | 
|  | // Real update view | 
|  | this._updateView(); | 
|  |  | 
|  | this._enableHoverLinkToSeries(); | 
|  | this._enableHoverLinkFromSeries(); | 
|  |  | 
|  | this.positionGroup(thisGroup); | 
|  | } | 
|  |  | 
|  | private _renderEndsText(group: graphic.Group, dataRangeText: string[], endsIndex?: 0 | 1) { | 
|  | if (!dataRangeText) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | // Compatible with ec2, text[0] map to high value, text[1] map low value. | 
|  | let text = dataRangeText[1 - endsIndex]; | 
|  | text = text != null ? text + '' : ''; | 
|  |  | 
|  | const visualMapModel = this.visualMapModel; | 
|  | const textGap = visualMapModel.get('textGap'); | 
|  | const itemSize = visualMapModel.itemSize; | 
|  |  | 
|  | const barGroup = this._shapes.mainGroup; | 
|  | const position = this._applyTransform( | 
|  | [ | 
|  | itemSize[0] / 2, | 
|  | endsIndex === 0 ? -textGap : itemSize[1] + textGap | 
|  | ], | 
|  | barGroup | 
|  | ) as number[]; | 
|  | const align = this._applyTransform( | 
|  | endsIndex === 0 ? 'bottom' : 'top', | 
|  | barGroup | 
|  | ); | 
|  | const orient = this._orient; | 
|  | const textStyleModel = this.visualMapModel.textStyleModel; | 
|  |  | 
|  | this.group.add(new graphic.Text({ | 
|  | style: createTextStyle(textStyleModel, { | 
|  | x: position[0], | 
|  | y: position[1], | 
|  | verticalAlign: orient === 'horizontal' ? 'middle' : align as TextVerticalAlign, | 
|  | align: orient === 'horizontal' ? align as TextAlign : 'center', | 
|  | text | 
|  | }) | 
|  | })); | 
|  | } | 
|  |  | 
|  | private _renderBar(targetGroup: graphic.Group) { | 
|  | const visualMapModel = this.visualMapModel; | 
|  | const shapes = this._shapes; | 
|  | const itemSize = visualMapModel.itemSize; | 
|  | const orient = this._orient; | 
|  | const useHandle = this._useHandle; | 
|  | const itemAlign = helper.getItemAlign(visualMapModel, this.api, itemSize); | 
|  | const mainGroup = shapes.mainGroup = this._createBarGroup(itemAlign); | 
|  |  | 
|  | const gradientBarGroup = new graphic.Group(); | 
|  | mainGroup.add(gradientBarGroup); | 
|  |  | 
|  | // Bar | 
|  | gradientBarGroup.add(shapes.outOfRange = createPolygon()); | 
|  | gradientBarGroup.add(shapes.inRange = createPolygon( | 
|  | null, | 
|  | useHandle ? getCursor(this._orient) : null, | 
|  | zrUtil.bind(this._dragHandle, this, 'all', false), | 
|  | zrUtil.bind(this._dragHandle, this, 'all', true) | 
|  | )); | 
|  |  | 
|  | // A border radius clip. | 
|  | gradientBarGroup.setClipPath(new graphic.Rect({ | 
|  | shape: { | 
|  | x: 0, | 
|  | y: 0, | 
|  | width: itemSize[0], | 
|  | height: itemSize[1], | 
|  | r: 3 | 
|  | } | 
|  | })); | 
|  |  | 
|  | const textRect = visualMapModel.textStyleModel.getTextRect('国'); | 
|  | const textSize = mathMax(textRect.width, textRect.height); | 
|  |  | 
|  | // Handle | 
|  | if (useHandle) { | 
|  | shapes.handleThumbs = []; | 
|  | shapes.handleLabels = []; | 
|  | shapes.handleLabelPoints = []; | 
|  |  | 
|  | this._createHandle(visualMapModel, mainGroup, 0, itemSize, textSize, orient); | 
|  | this._createHandle(visualMapModel, mainGroup, 1, itemSize, textSize, orient); | 
|  | } | 
|  |  | 
|  | this._createIndicator(visualMapModel, mainGroup, itemSize, textSize, orient); | 
|  |  | 
|  | targetGroup.add(mainGroup); | 
|  | } | 
|  |  | 
|  | private _createHandle( | 
|  | visualMapModel: ContinuousModel, | 
|  | mainGroup: graphic.Group, | 
|  | handleIndex: 0 | 1, | 
|  | itemSize: number[], | 
|  | textSize: number, | 
|  | orient: Orient | 
|  | ) { | 
|  | const onDrift = zrUtil.bind(this._dragHandle, this, handleIndex, false); | 
|  | const onDragEnd = zrUtil.bind(this._dragHandle, this, handleIndex, true); | 
|  | const handleSize = parsePercent(visualMapModel.get('handleSize'), itemSize[0]); | 
|  | const handleThumb = createSymbol( | 
|  | visualMapModel.get('handleIcon'), | 
|  | -handleSize / 2, -handleSize / 2, handleSize, handleSize, | 
|  | null, true | 
|  | ); | 
|  | const cursor = getCursor(this._orient); | 
|  | handleThumb.attr({ | 
|  | cursor: cursor, | 
|  | draggable: true, | 
|  | drift: onDrift, | 
|  | ondragend: onDragEnd, | 
|  | onmousemove(e) { | 
|  | eventTool.stop(e.event); | 
|  | } | 
|  | }); | 
|  | handleThumb.x = itemSize[0] / 2; | 
|  |  | 
|  | handleThumb.useStyle(visualMapModel.getModel('handleStyle').getItemStyle()); | 
|  | (handleThumb as graphic.Path).setStyle({ | 
|  | strokeNoScale: true, | 
|  | strokeFirst: true | 
|  | }); | 
|  | (handleThumb as graphic.Path).style.lineWidth *= 2; | 
|  |  | 
|  | handleThumb.ensureState('emphasis').style = visualMapModel.getModel(['emphasis', 'handleStyle']).getItemStyle(); | 
|  | setAsHighDownDispatcher(handleThumb, true); | 
|  |  | 
|  | mainGroup.add(handleThumb); | 
|  |  | 
|  | // Text is always horizontal layout but should not be effected by | 
|  | // transform (orient/inverse). So label is built separately but not | 
|  | // use zrender/graphic/helper/RectText, and is located based on view | 
|  | // group (according to handleLabelPoint) but not barGroup. | 
|  | const textStyleModel = this.visualMapModel.textStyleModel; | 
|  | const handleLabel = new graphic.Text({ | 
|  | cursor: cursor, | 
|  | draggable: true, | 
|  | drift: onDrift, | 
|  | onmousemove(e) { | 
|  | // Fot mobile devicem, prevent screen slider on the button. | 
|  | eventTool.stop(e.event); | 
|  | }, | 
|  | ondragend: onDragEnd, | 
|  | style: createTextStyle(textStyleModel, { | 
|  | x: 0, | 
|  | y: 0, | 
|  | text: '' | 
|  | }) | 
|  | }); | 
|  | handleLabel.ensureState('blur').style = { | 
|  | opacity: 0.1 | 
|  | }; | 
|  | handleLabel.stateTransition = { duration: 200 }; | 
|  |  | 
|  | this.group.add(handleLabel); | 
|  |  | 
|  | const handleLabelPoint = [handleSize, 0]; | 
|  |  | 
|  | const shapes = this._shapes; | 
|  | shapes.handleThumbs[handleIndex] = handleThumb; | 
|  | shapes.handleLabelPoints[handleIndex] = handleLabelPoint; | 
|  | shapes.handleLabels[handleIndex] = handleLabel; | 
|  | } | 
|  |  | 
|  | private _createIndicator( | 
|  | visualMapModel: ContinuousModel, | 
|  | mainGroup: graphic.Group, | 
|  | itemSize: number[], | 
|  | textSize: number, | 
|  | orient: Orient | 
|  | ) { | 
|  | const scale = parsePercent(visualMapModel.get('indicatorSize'), itemSize[0]); | 
|  | const indicator = createSymbol( | 
|  | visualMapModel.get('indicatorIcon'), | 
|  | -scale / 2, -scale / 2, scale, scale, | 
|  | null, true | 
|  | ); | 
|  | indicator.attr({ | 
|  | cursor: 'move', | 
|  | invisible: true, | 
|  | silent: true, | 
|  | x: itemSize[0] / 2 | 
|  | }); | 
|  | const indicatorStyle = visualMapModel.getModel('indicatorStyle').getItemStyle(); | 
|  | if (indicator instanceof ZRImage) { | 
|  | const pathStyle = indicator.style; | 
|  | indicator.useStyle(zrUtil.extend({ | 
|  | // TODO other properties like x, y ? | 
|  | image: pathStyle.image, | 
|  | x: pathStyle.x, y: pathStyle.y, | 
|  | width: pathStyle.width, height: pathStyle.height | 
|  | }, indicatorStyle)); | 
|  | } | 
|  | else { | 
|  | indicator.useStyle(indicatorStyle); | 
|  | } | 
|  |  | 
|  | mainGroup.add(indicator); | 
|  |  | 
|  | const textStyleModel = this.visualMapModel.textStyleModel; | 
|  | const indicatorLabel = new graphic.Text({ | 
|  | silent: true, | 
|  | invisible: true, | 
|  | style: createTextStyle(textStyleModel, { | 
|  | x: 0, | 
|  | y: 0, | 
|  | text: '' | 
|  | }) | 
|  | }); | 
|  | this.group.add(indicatorLabel); | 
|  |  | 
|  | const indicatorLabelPoint = [ | 
|  | (orient === 'horizontal' ? textSize / 2 : HOVER_LINK_OUT) + itemSize[0] / 2, | 
|  | 0 | 
|  | ]; | 
|  |  | 
|  | const shapes = this._shapes; | 
|  | shapes.indicator = indicator; | 
|  | shapes.indicatorLabel = indicatorLabel; | 
|  | shapes.indicatorLabelPoint = indicatorLabelPoint; | 
|  |  | 
|  | this._firstShowIndicator = true; | 
|  | } | 
|  |  | 
|  | private _dragHandle( | 
|  | handleIndex: 0 | 1 | 'all', | 
|  | isEnd?: boolean, | 
|  | // dx is event from ondragend if isEnd is true. It's not used | 
|  | dx?: number | ElementEvent, | 
|  | dy?: number | 
|  | ) { | 
|  | if (!this._useHandle) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | this._dragging = !isEnd; | 
|  |  | 
|  | if (!isEnd) { | 
|  | // Transform dx, dy to bar coordination. | 
|  | const vertex = this._applyTransform([dx as number, dy], this._shapes.mainGroup, true) as number[]; | 
|  | this._updateInterval(handleIndex, vertex[1]); | 
|  |  | 
|  | this._hideIndicator(); | 
|  | // Considering realtime, update view should be executed | 
|  | // before dispatch action. | 
|  | this._updateView(); | 
|  | } | 
|  |  | 
|  | // dragEnd do not dispatch action when realtime. | 
|  | if (isEnd === !this.visualMapModel.get('realtime')) { // jshint ignore:line | 
|  | this.api.dispatchAction({ | 
|  | type: 'selectDataRange', | 
|  | from: this.uid, | 
|  | visualMapId: this.visualMapModel.id, | 
|  | selected: this._dataInterval.slice() | 
|  | }); | 
|  | } | 
|  |  | 
|  | if (isEnd) { | 
|  | !this._hovering && this._clearHoverLinkToSeries(); | 
|  | } | 
|  | else if (useHoverLinkOnHandle(this.visualMapModel)) { | 
|  | this._doHoverLinkToSeries(this._handleEnds[handleIndex as 0 | 1], false); | 
|  | } | 
|  | } | 
|  |  | 
|  | private _resetInterval() { | 
|  | const visualMapModel = this.visualMapModel; | 
|  |  | 
|  | const dataInterval = this._dataInterval = visualMapModel.getSelected(); | 
|  | const dataExtent = visualMapModel.getExtent(); | 
|  | const sizeExtent = [0, visualMapModel.itemSize[1]]; | 
|  |  | 
|  | this._handleEnds = [ | 
|  | linearMap(dataInterval[0], dataExtent, sizeExtent, true), | 
|  | linearMap(dataInterval[1], dataExtent, sizeExtent, true) | 
|  | ]; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @private | 
|  | * @param {(number|string)} handleIndex 0 or 1 or 'all' | 
|  | * @param {number} dx | 
|  | * @param {number} dy | 
|  | */ | 
|  | private _updateInterval(handleIndex: 0 | 1 | 'all', delta: number) { | 
|  | delta = delta || 0; | 
|  | const visualMapModel = this.visualMapModel; | 
|  | const handleEnds = this._handleEnds; | 
|  | const sizeExtent = [0, visualMapModel.itemSize[1]]; | 
|  |  | 
|  | sliderMove( | 
|  | delta, | 
|  | handleEnds, | 
|  | sizeExtent, | 
|  | handleIndex, | 
|  | // cross is forbiden | 
|  | 0 | 
|  | ); | 
|  |  | 
|  | const dataExtent = visualMapModel.getExtent(); | 
|  | // Update data interval. | 
|  | this._dataInterval = [ | 
|  | linearMap(handleEnds[0], sizeExtent, dataExtent, true), | 
|  | linearMap(handleEnds[1], sizeExtent, dataExtent, true) | 
|  | ]; | 
|  | } | 
|  |  | 
|  | private _updateView(forSketch?: boolean) { | 
|  | const visualMapModel = this.visualMapModel; | 
|  | const dataExtent = visualMapModel.getExtent(); | 
|  | const shapes = this._shapes; | 
|  |  | 
|  | const outOfRangeHandleEnds = [0, visualMapModel.itemSize[1]]; | 
|  | const inRangeHandleEnds = forSketch ? outOfRangeHandleEnds : this._handleEnds; | 
|  |  | 
|  | const visualInRange = this._createBarVisual( | 
|  | this._dataInterval, dataExtent, inRangeHandleEnds, 'inRange' | 
|  | ); | 
|  | const visualOutOfRange = this._createBarVisual( | 
|  | dataExtent, dataExtent, outOfRangeHandleEnds, 'outOfRange' | 
|  | ); | 
|  |  | 
|  | shapes.inRange | 
|  | .setStyle({ | 
|  | fill: visualInRange.barColor | 
|  | // opacity: visualInRange.opacity | 
|  | }) | 
|  | .setShape('points', visualInRange.barPoints); | 
|  | shapes.outOfRange | 
|  | .setStyle({ | 
|  | fill: visualOutOfRange.barColor | 
|  | // opacity: visualOutOfRange.opacity | 
|  | }) | 
|  | .setShape('points', visualOutOfRange.barPoints); | 
|  |  | 
|  | this._updateHandle(inRangeHandleEnds, visualInRange); | 
|  | } | 
|  |  | 
|  | private _createBarVisual( | 
|  | dataInterval: number[], | 
|  | dataExtent: number[], | 
|  | handleEnds: number[], | 
|  | forceState: ContinuousModel['stateList'][number] | 
|  | ): BarVisual { | 
|  | const opts = { | 
|  | forceState: forceState, | 
|  | convertOpacityToAlpha: true | 
|  | }; | 
|  | const colorStops = this._makeColorGradient(dataInterval, opts); | 
|  |  | 
|  | const symbolSizes = [ | 
|  | this.getControllerVisual(dataInterval[0], 'symbolSize', opts) as number, | 
|  | this.getControllerVisual(dataInterval[1], 'symbolSize', opts) as number | 
|  | ]; | 
|  | const barPoints = this._createBarPoints(handleEnds, symbolSizes); | 
|  |  | 
|  | return { | 
|  | barColor: new LinearGradient(0, 0, 0, 1, colorStops), | 
|  | barPoints: barPoints, | 
|  | handlesColor: [ | 
|  | colorStops[0].color, | 
|  | colorStops[colorStops.length - 1].color | 
|  | ] | 
|  | }; | 
|  | } | 
|  |  | 
|  | private _makeColorGradient( | 
|  | dataInterval: number[], | 
|  | opts: { | 
|  | forceState?: ContinuousModel['stateList'][number] | 
|  | convertOpacityToAlpha?: boolean | 
|  | } | 
|  | ) { | 
|  | // Considering colorHue, which is not linear, so we have to sample | 
|  | // to calculate gradient color stops, but not only caculate head | 
|  | // and tail. | 
|  | const sampleNumber = 100; // Arbitrary value. | 
|  | const colorStops: LinearGradientObject['colorStops'] = []; | 
|  | const step = (dataInterval[1] - dataInterval[0]) / sampleNumber; | 
|  |  | 
|  | colorStops.push({ | 
|  | color: this.getControllerVisual(dataInterval[0], 'color', opts) as ColorString, | 
|  | offset: 0 | 
|  | }); | 
|  |  | 
|  | for (let i = 1; i < sampleNumber; i++) { | 
|  | const currValue = dataInterval[0] + step * i; | 
|  | if (currValue > dataInterval[1]) { | 
|  | break; | 
|  | } | 
|  | colorStops.push({ | 
|  | color: this.getControllerVisual(currValue, 'color', opts) as ColorString, | 
|  | offset: i / sampleNumber | 
|  | }); | 
|  | } | 
|  |  | 
|  | colorStops.push({ | 
|  | color: this.getControllerVisual(dataInterval[1], 'color', opts) as ColorString, | 
|  | offset: 1 | 
|  | }); | 
|  |  | 
|  | return colorStops; | 
|  | } | 
|  |  | 
|  | private _createBarPoints(handleEnds: number[], symbolSizes: number[]) { | 
|  | const itemSize = this.visualMapModel.itemSize; | 
|  |  | 
|  | return [ | 
|  | [itemSize[0] - symbolSizes[0], handleEnds[0]], | 
|  | [itemSize[0], handleEnds[0]], | 
|  | [itemSize[0], handleEnds[1]], | 
|  | [itemSize[0] - symbolSizes[1], handleEnds[1]] | 
|  | ]; | 
|  | } | 
|  |  | 
|  | private _createBarGroup(itemAlign: helper.ItemAlign) { | 
|  | const orient = this._orient; | 
|  | const inverse = this.visualMapModel.get('inverse'); | 
|  |  | 
|  | return new graphic.Group( | 
|  | (orient === 'horizontal' && !inverse) | 
|  | ? {scaleX: itemAlign === 'bottom' ? 1 : -1, rotation: Math.PI / 2} | 
|  | : (orient === 'horizontal' && inverse) | 
|  | ? {scaleX: itemAlign === 'bottom' ? -1 : 1, rotation: -Math.PI / 2} | 
|  | : (orient === 'vertical' && !inverse) | 
|  | ? {scaleX: itemAlign === 'left' ? 1 : -1, scaleY: -1} | 
|  | : {scaleX: itemAlign === 'left' ? 1 : -1} | 
|  | ); | 
|  | } | 
|  |  | 
|  | private _updateHandle(handleEnds: number[], visualInRange: BarVisual) { | 
|  | if (!this._useHandle) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | const shapes = this._shapes; | 
|  | const visualMapModel = this.visualMapModel; | 
|  | const handleThumbs = shapes.handleThumbs; | 
|  | const handleLabels = shapes.handleLabels; | 
|  | const itemSize = visualMapModel.itemSize; | 
|  | const dataExtent = visualMapModel.getExtent(); | 
|  |  | 
|  | each([0, 1], function (handleIndex) { | 
|  | const handleThumb = handleThumbs[handleIndex]; | 
|  | handleThumb.setStyle('fill', visualInRange.handlesColor[handleIndex]); | 
|  | handleThumb.y = handleEnds[handleIndex]; | 
|  |  | 
|  | const val = linearMap(handleEnds[handleIndex], [0, itemSize[1]], dataExtent, true); | 
|  | const symbolSize = this.getControllerVisual(val, 'symbolSize') as number; | 
|  |  | 
|  | handleThumb.scaleX = handleThumb.scaleY = symbolSize / itemSize[0]; | 
|  | handleThumb.x = itemSize[0] - symbolSize / 2; | 
|  |  | 
|  | // Update handle label position. | 
|  | const textPoint = graphic.applyTransform( | 
|  | shapes.handleLabelPoints[handleIndex], | 
|  | graphic.getTransform(handleThumb, this.group) | 
|  | ); | 
|  | handleLabels[handleIndex].setStyle({ | 
|  | x: textPoint[0], | 
|  | y: textPoint[1], | 
|  | text: visualMapModel.formatValueText(this._dataInterval[handleIndex]), | 
|  | verticalAlign: 'middle', | 
|  | align: this._orient === 'vertical' ? this._applyTransform( | 
|  | 'left', | 
|  | shapes.mainGroup | 
|  | ) as TextAlign : 'center' | 
|  | }); | 
|  | }, this); | 
|  | } | 
|  |  | 
|  | private _showIndicator( | 
|  | cursorValue: number, | 
|  | textValue: number, | 
|  | rangeSymbol?: string, | 
|  | halfHoverLinkSize?: number | 
|  | ) { | 
|  | const visualMapModel = this.visualMapModel; | 
|  | const dataExtent = visualMapModel.getExtent(); | 
|  | const itemSize = visualMapModel.itemSize; | 
|  | const sizeExtent = [0, itemSize[1]]; | 
|  |  | 
|  | const shapes = this._shapes; | 
|  | const indicator = shapes.indicator; | 
|  | if (!indicator) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | indicator.attr('invisible', false); | 
|  |  | 
|  | const opts = {convertOpacityToAlpha: true}; | 
|  | const color = this.getControllerVisual(cursorValue, 'color', opts) as ColorString; | 
|  | const symbolSize = this.getControllerVisual(cursorValue, 'symbolSize') as number; | 
|  | const y = linearMap(cursorValue, dataExtent, sizeExtent, true); | 
|  | const x = itemSize[0] - symbolSize / 2; | 
|  |  | 
|  | const oldIndicatorPos = { x: indicator.x, y: indicator.y }; | 
|  | // Update handle label position. | 
|  | indicator.y = y; | 
|  | indicator.x = x; | 
|  | const textPoint = graphic.applyTransform( | 
|  | shapes.indicatorLabelPoint, | 
|  | graphic.getTransform(indicator, this.group) | 
|  | ); | 
|  |  | 
|  | const indicatorLabel = shapes.indicatorLabel; | 
|  | indicatorLabel.attr('invisible', false); | 
|  | const align = this._applyTransform('left', shapes.mainGroup); | 
|  | const orient = this._orient; | 
|  | const isHorizontal = orient === 'horizontal'; | 
|  | indicatorLabel.setStyle({ | 
|  | text: (rangeSymbol ? rangeSymbol : '') + visualMapModel.formatValueText(textValue), | 
|  | verticalAlign: isHorizontal ? align as TextVerticalAlign : 'middle', | 
|  | align: isHorizontal ? 'center' : align as TextAlign | 
|  | }); | 
|  |  | 
|  | const indicatorNewProps = { | 
|  | x: x, | 
|  | y: y, | 
|  | style: { | 
|  | fill: color | 
|  | } | 
|  | }; | 
|  | const labelNewProps = { | 
|  | style: { | 
|  | x: textPoint[0], | 
|  | y: textPoint[1] | 
|  | } | 
|  | }; | 
|  |  | 
|  | if (visualMapModel.ecModel.isAnimationEnabled() && !this._firstShowIndicator) { | 
|  | const animationCfg = { | 
|  | duration: 100, | 
|  | easing: 'cubicInOut', | 
|  | additive: true | 
|  | } as const; | 
|  | indicator.x = oldIndicatorPos.x; | 
|  | indicator.y = oldIndicatorPos.y; | 
|  | indicator.animateTo(indicatorNewProps, animationCfg); | 
|  | indicatorLabel.animateTo(labelNewProps, animationCfg); | 
|  | } | 
|  | else { | 
|  | indicator.attr(indicatorNewProps); | 
|  | indicatorLabel.attr(labelNewProps); | 
|  | } | 
|  |  | 
|  | this._firstShowIndicator = false; | 
|  |  | 
|  | const handleLabels = this._shapes.handleLabels; | 
|  | if (handleLabels) { | 
|  | for (let i = 0; i < handleLabels.length; i++) { | 
|  | // Fade out handle labels. | 
|  | // NOTE: Must use api enter/leave on emphasis/blur/select state. Or the global states manager will change it. | 
|  | this._api.enterBlur(handleLabels[i]); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | private _enableHoverLinkToSeries() { | 
|  | const self = this; | 
|  | this._shapes.mainGroup | 
|  |  | 
|  | .on('mousemove', function (e) { | 
|  | self._hovering = true; | 
|  |  | 
|  | if (!self._dragging) { | 
|  | const itemSize = self.visualMapModel.itemSize; | 
|  | const pos = self._applyTransform( | 
|  | [e.offsetX, e.offsetY], self._shapes.mainGroup, true, true | 
|  | ); | 
|  | // For hover link show when hover handle, which might be | 
|  | // below or upper than sizeExtent. | 
|  | pos[1] = mathMin(mathMax(0, pos[1]), itemSize[1]); | 
|  | self._doHoverLinkToSeries( | 
|  | pos[1], | 
|  | 0 <= pos[0] && pos[0] <= itemSize[0] | 
|  | ); | 
|  | } | 
|  | }) | 
|  |  | 
|  | .on('mouseout', function () { | 
|  | // When mouse is out of handle, hoverLink still need | 
|  | // to be displayed when realtime is set as false. | 
|  | self._hovering = false; | 
|  | !self._dragging && self._clearHoverLinkToSeries(); | 
|  | }); | 
|  | } | 
|  |  | 
|  | private _enableHoverLinkFromSeries() { | 
|  | const zr = this.api.getZr(); | 
|  |  | 
|  | if (this.visualMapModel.option.hoverLink) { | 
|  | zr.on('mouseover', this._hoverLinkFromSeriesMouseOver, this); | 
|  | zr.on('mouseout', this._hideIndicator, this); | 
|  | } | 
|  | else { | 
|  | this._clearHoverLinkFromSeries(); | 
|  | } | 
|  | } | 
|  |  | 
|  | private _doHoverLinkToSeries(cursorPos: number, hoverOnBar?: boolean) { | 
|  | const visualMapModel = this.visualMapModel; | 
|  | const itemSize = visualMapModel.itemSize; | 
|  |  | 
|  | if (!visualMapModel.option.hoverLink) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | const sizeExtent = [0, itemSize[1]]; | 
|  | const dataExtent = visualMapModel.getExtent(); | 
|  |  | 
|  | // For hover link show when hover handle, which might be below or upper than sizeExtent. | 
|  | cursorPos = mathMin(mathMax(sizeExtent[0], cursorPos), sizeExtent[1]); | 
|  |  | 
|  | const halfHoverLinkSize = getHalfHoverLinkSize(visualMapModel, dataExtent, sizeExtent); | 
|  | const hoverRange = [cursorPos - halfHoverLinkSize, cursorPos + halfHoverLinkSize]; | 
|  | const cursorValue = linearMap(cursorPos, sizeExtent, dataExtent, true); | 
|  | const valueRange = [ | 
|  | linearMap(hoverRange[0], sizeExtent, dataExtent, true), | 
|  | linearMap(hoverRange[1], sizeExtent, dataExtent, true) | 
|  | ]; | 
|  | // Consider data range is out of visualMap range, see test/visualMap-continuous.html, | 
|  | // where china and india has very large population. | 
|  | hoverRange[0] < sizeExtent[0] && (valueRange[0] = -Infinity); | 
|  | hoverRange[1] > sizeExtent[1] && (valueRange[1] = Infinity); | 
|  |  | 
|  | // Do not show indicator when mouse is over handle, | 
|  | // otherwise labels overlap, especially when dragging. | 
|  | if (hoverOnBar) { | 
|  | if (valueRange[0] === -Infinity) { | 
|  | this._showIndicator(cursorValue, valueRange[1], '< ', halfHoverLinkSize); | 
|  | } | 
|  | else if (valueRange[1] === Infinity) { | 
|  | this._showIndicator(cursorValue, valueRange[0], '> ', halfHoverLinkSize); | 
|  | } | 
|  | else { | 
|  | this._showIndicator(cursorValue, cursorValue, '≈ ', halfHoverLinkSize); | 
|  | } | 
|  | } | 
|  |  | 
|  | // When realtime is set as false, handles, which are in barGroup, | 
|  | // also trigger hoverLink, which help user to realize where they | 
|  | // focus on when dragging. (see test/heatmap-large.html) | 
|  | // When realtime is set as true, highlight will not show when hover | 
|  | // handle, because the label on handle, which displays a exact value | 
|  | // but not range, might mislead users. | 
|  | const oldBatch = this._hoverLinkDataIndices; | 
|  | let newBatch: TargetDataIndices = []; | 
|  | if (hoverOnBar || useHoverLinkOnHandle(visualMapModel)) { | 
|  | newBatch = this._hoverLinkDataIndices = visualMapModel.findTargetDataIndices(valueRange); | 
|  | } | 
|  |  | 
|  | const resultBatches = modelUtil.compressBatches(oldBatch, newBatch); | 
|  |  | 
|  | this._dispatchHighDown('downplay', helper.makeHighDownBatch(resultBatches[0], visualMapModel)); | 
|  | this._dispatchHighDown('highlight', helper.makeHighDownBatch(resultBatches[1], visualMapModel)); | 
|  | } | 
|  |  | 
|  | private _hoverLinkFromSeriesMouseOver(e: ElementEvent) { | 
|  | let ecData: ECData; | 
|  |  | 
|  | findEventDispatcher(e.target, target => { | 
|  | const currECData = getECData(target); | 
|  | if (currECData.dataIndex != null) { | 
|  | ecData = currECData; | 
|  | return true; | 
|  | } | 
|  | }, true); | 
|  |  | 
|  | if (!ecData) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | const dataModel = this.ecModel.getSeriesByIndex(ecData.seriesIndex); | 
|  |  | 
|  | const visualMapModel = this.visualMapModel; | 
|  | if (!visualMapModel.isTargetSeries(dataModel)) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | const data = dataModel.getData(ecData.dataType); | 
|  | const value = data.getStore().get(visualMapModel.getDataDimensionIndex(data), ecData.dataIndex) as number; | 
|  |  | 
|  | if (!isNaN(value)) { | 
|  | this._showIndicator(value, value); | 
|  | } | 
|  | } | 
|  |  | 
|  | private _hideIndicator() { | 
|  | const shapes = this._shapes; | 
|  | shapes.indicator && shapes.indicator.attr('invisible', true); | 
|  | shapes.indicatorLabel && shapes.indicatorLabel.attr('invisible', true); | 
|  |  | 
|  | const handleLabels = this._shapes.handleLabels; | 
|  | if (handleLabels) { | 
|  | for (let i = 0; i < handleLabels.length; i++) { | 
|  | // Fade out handle labels. | 
|  | // NOTE: Must use api enter/leave on emphasis/blur/select state. Or the global states manager will change it. | 
|  | this._api.leaveBlur(handleLabels[i]); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | private _clearHoverLinkToSeries() { | 
|  | this._hideIndicator(); | 
|  |  | 
|  | const indices = this._hoverLinkDataIndices; | 
|  | this._dispatchHighDown('downplay', helper.makeHighDownBatch(indices, this.visualMapModel)); | 
|  |  | 
|  | indices.length = 0; | 
|  | } | 
|  |  | 
|  | private _clearHoverLinkFromSeries() { | 
|  | this._hideIndicator(); | 
|  |  | 
|  | const zr = this.api.getZr(); | 
|  | zr.off('mouseover', this._hoverLinkFromSeriesMouseOver); | 
|  | zr.off('mouseout', this._hideIndicator); | 
|  | } | 
|  | private _applyTransform(vertex: number[], element: Element, inverse?: boolean, global?: boolean): number[] | 
|  | private _applyTransform(vertex: Direction, element: Element, inverse?: boolean, global?: boolean): Direction | 
|  | private _applyTransform( | 
|  | vertex: number[] | Direction, | 
|  | element: Element, | 
|  | inverse?: boolean, | 
|  | global?: boolean | 
|  | ) { | 
|  | const transform = graphic.getTransform(element, global ? null : this.group); | 
|  |  | 
|  | return zrUtil.isArray(vertex) | 
|  | ? graphic.applyTransform(vertex, transform, inverse) | 
|  | : graphic.transformDirection(vertex, transform, inverse); | 
|  | } | 
|  |  | 
|  | // TODO: TYPE more specified payload types. | 
|  | private _dispatchHighDown(type: 'highlight' | 'downplay', batch: Payload['batch']) { | 
|  | batch && batch.length && this.api.dispatchAction({ | 
|  | type: type, | 
|  | batch: batch | 
|  | }); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @override | 
|  | */ | 
|  | dispose() { | 
|  | this._clearHoverLinkFromSeries(); | 
|  | this._clearHoverLinkToSeries(); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @override | 
|  | */ | 
|  | remove() { | 
|  | this._clearHoverLinkFromSeries(); | 
|  | this._clearHoverLinkToSeries(); | 
|  | } | 
|  |  | 
|  | } | 
|  |  | 
|  | function createPolygon( | 
|  | points?: number[][], | 
|  | cursor?: string, | 
|  | onDrift?: (x: number, y: number) => void, | 
|  | onDragEnd?: () => void | 
|  | ) { | 
|  | return new graphic.Polygon({ | 
|  | shape: {points: points}, | 
|  | draggable: !!onDrift, | 
|  | cursor: cursor, | 
|  | drift: onDrift, | 
|  | onmousemove(e) { | 
|  | // Fot mobile devicem, prevent screen slider on the button. | 
|  | eventTool.stop(e.event); | 
|  | }, | 
|  | ondragend: onDragEnd | 
|  | }); | 
|  | } | 
|  |  | 
|  | function getHalfHoverLinkSize(visualMapModel: ContinuousModel, dataExtent: number[], sizeExtent: number[]) { | 
|  | let halfHoverLinkSize = HOVER_LINK_SIZE / 2; | 
|  | const hoverLinkDataSize = visualMapModel.get('hoverLinkDataSize'); | 
|  | if (hoverLinkDataSize) { | 
|  | halfHoverLinkSize = linearMap(hoverLinkDataSize, dataExtent, sizeExtent, true) / 2; | 
|  | } | 
|  | return halfHoverLinkSize; | 
|  | } | 
|  |  | 
|  | function useHoverLinkOnHandle(visualMapModel: ContinuousModel) { | 
|  | const hoverLinkOnHandle = visualMapModel.get('hoverLinkOnHandle'); | 
|  | return !!(hoverLinkOnHandle == null ? visualMapModel.get('realtime') : hoverLinkOnHandle); | 
|  | } | 
|  |  | 
|  | function getCursor(orient: Orient) { | 
|  | return orient === 'vertical' ? 'ns-resize' : 'ew-resize'; | 
|  | } | 
|  |  | 
|  | export default ContinuousView; |