/* * 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; 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) { // For mobile device, 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 forbidden 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 calculate 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) { // For mobile device, 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;