/* * 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 {bind, each, isFunction, isString, indexOf} from 'zrender/src/core/util'; import * as eventTool from 'zrender/src/core/event'; import * as graphic from '../../util/graphic'; import * as throttle from '../../util/throttle'; import DataZoomView from './DataZoomView'; import {linearMap, asc, parsePercent} from '../../util/number'; import * as layout from '../../util/layout'; import sliderMove from '../helper/sliderMove'; import GlobalModel from '../../model/Global'; import ExtensionAPI from '../../core/ExtensionAPI'; import { LayoutOrient, Payload, ZRTextVerticalAlign, ZRTextAlign, ZRElementEvent, ParsedValue } from '../../util/types'; import SliderZoomModel from './SliderZoomModel'; import { RectLike } from 'zrender/src/core/BoundingRect'; import Axis from '../../coord/Axis'; import SeriesModel from '../../model/Series'; import { AxisBaseModel } from '../../coord/AxisBaseModel'; import { getAxisMainType, collectReferCoordSysModelInfo } from './helper'; import { enableHoverEmphasis } from '../../util/states'; import { createSymbol, symbolBuildProxies } from '../../util/symbol'; import { deprecateLog } from '../../util/log'; import { PointLike } from 'zrender/src/core/Point'; import Displayable from 'zrender/src/graphic/Displayable'; import {createTextStyle} from '../../label/labelStyle'; import SeriesData from '../../data/SeriesData'; const Rect = graphic.Rect; // Constants const DEFAULT_LOCATION_EDGE_GAP = 7; const DEFAULT_FRAME_BORDER_WIDTH = 1; const DEFAULT_FILLER_SIZE = 30; const DEFAULT_MOVE_HANDLE_SIZE = 7; const HORIZONTAL = 'horizontal'; const VERTICAL = 'vertical'; const LABEL_GAP = 5; const SHOW_DATA_SHADOW_SERIES_TYPE = ['line', 'bar', 'candlestick', 'scatter']; const REALTIME_ANIMATION_CONFIG = { easing: 'cubicOut', duration: 100, delay: 0 } as const; // const NORMAL_ANIMATION_CONFIG = { // easing: 'cubicInOut', // duration: 200 // } as const; interface Displayables { sliderGroup: graphic.Group; handles: [graphic.Path, graphic.Path]; handleLabels: [graphic.Text, graphic.Text]; dataShadowSegs: graphic.Group[]; filler: graphic.Rect; brushRect: graphic.Rect; moveHandle: graphic.Rect; moveHandleIcon: graphic.Path; // invisible move zone. moveZone: graphic.Rect; } class SliderZoomView extends DataZoomView { static type = 'dataZoom.slider'; type = SliderZoomView.type; dataZoomModel: SliderZoomModel; private _displayables = {} as Displayables; private _orient: LayoutOrient; private _range: number[]; /** * [coord of the first handle, coord of the second handle] */ private _handleEnds: number[]; /** * [length, thick] */ private _size: number[]; private _handleWidth: number; private _handleHeight: number; private _location: PointLike; private _brushStart: PointLike; private _brushStartTime: number; private _dragging: boolean; private _brushing: boolean; private _dataShadowInfo: { thisAxis: Axis series: SeriesModel thisDim: string otherDim: string otherAxisInverse: boolean }; // Cached raw data. Avoid rendering data shadow multiple times. private _shadowData: SeriesData; private _shadowDim: string; private _shadowSize: number[]; private _shadowPolygonPts: number[][]; private _shadowPolylinePts: number[][]; init(ecModel: GlobalModel, api: ExtensionAPI) { this.api = api; // A unique handler for each dataZoom component this._onBrush = bind(this._onBrush, this); this._onBrushEnd = bind(this._onBrushEnd, this); } render( dataZoomModel: SliderZoomModel, ecModel: GlobalModel, api: ExtensionAPI, payload: Payload & { from: string type: string } ) { super.render.apply(this, arguments as any); throttle.createOrUpdate( this, '_dispatchZoomAction', dataZoomModel.get('throttle'), 'fixRate' ); this._orient = dataZoomModel.getOrient(); if (dataZoomModel.get('show') === false) { this.group.removeAll(); return; } if (dataZoomModel.noTarget()) { this._clear(); this.group.removeAll(); return; } // Notice: this._resetInterval() should not be executed when payload.type // is 'dataZoom', origin this._range should be maintained, otherwise 'pan' // or 'zoom' info will be missed because of 'throttle' of this.dispatchAction, if (!payload || payload.type !== 'dataZoom' || payload.from !== this.uid) { this._buildView(); } this._updateView(); } dispose() { this._clear(); super.dispose.apply(this, arguments as any); } private _clear() { throttle.clear(this, '_dispatchZoomAction'); const zr = this.api.getZr(); zr.off('mousemove', this._onBrush); zr.off('mouseup', this._onBrushEnd); } private _buildView() { const thisGroup = this.group; thisGroup.removeAll(); this._brushing = false; this._displayables.brushRect = null; this._resetLocation(); this._resetInterval(); const barGroup = this._displayables.sliderGroup = new graphic.Group(); this._renderBackground(); this._renderHandle(); this._renderDataShadow(); thisGroup.add(barGroup); this._positionGroup(); } private _resetLocation() { const dataZoomModel = this.dataZoomModel; const api = this.api; const showMoveHandle = dataZoomModel.get('brushSelect'); const moveHandleSize = showMoveHandle ? DEFAULT_MOVE_HANDLE_SIZE : 0; // If some of x/y/width/height are not specified, // auto-adapt according to target grid. const coordRect = this._findCoordRect(); const ecSize = {width: api.getWidth(), height: api.getHeight()}; // Default align by coordinate system rect. const positionInfo = this._orient === HORIZONTAL ? { // Why using 'right', because right should be used in vertical, // and it is better to be consistent for dealing with position param merge. right: ecSize.width - coordRect.x - coordRect.width, top: (ecSize.height - DEFAULT_FILLER_SIZE - DEFAULT_LOCATION_EDGE_GAP - moveHandleSize), width: coordRect.width, height: DEFAULT_FILLER_SIZE } : { // vertical right: DEFAULT_LOCATION_EDGE_GAP, top: coordRect.y, width: DEFAULT_FILLER_SIZE, height: coordRect.height }; // Do not write back to option and replace value 'ph', because // the 'ph' value should be recalculated when resize. const layoutParams = layout.getLayoutParams(dataZoomModel.option); // Replace the placeholder value. each(['right', 'top', 'width', 'height'] as const, function (name) { if (layoutParams[name] === 'ph') { layoutParams[name] = positionInfo[name]; } }); const layoutRect = layout.getLayoutRect( layoutParams, ecSize ); this._location = {x: layoutRect.x, y: layoutRect.y}; this._size = [layoutRect.width, layoutRect.height]; this._orient === VERTICAL && this._size.reverse(); } private _positionGroup() { const thisGroup = this.group; const location = this._location; const orient = this._orient; // Just use the first axis to determine mapping. const targetAxisModel = this.dataZoomModel.getFirstTargetAxisModel(); const inverse = targetAxisModel && targetAxisModel.get('inverse'); const sliderGroup = this._displayables.sliderGroup; const otherAxisInverse = (this._dataShadowInfo || {}).otherAxisInverse; // Transform barGroup. sliderGroup.attr( (orient === HORIZONTAL && !inverse) ? {scaleY: otherAxisInverse ? 1 : -1, scaleX: 1 } : (orient === HORIZONTAL && inverse) ? {scaleY: otherAxisInverse ? 1 : -1, scaleX: -1 } : (orient === VERTICAL && !inverse) ? {scaleY: otherAxisInverse ? -1 : 1, scaleX: 1, rotation: Math.PI / 2} // Don't use Math.PI, considering shadow direction. : {scaleY: otherAxisInverse ? -1 : 1, scaleX: -1, rotation: Math.PI / 2} ); // Position barGroup const rect = thisGroup.getBoundingRect([sliderGroup]); thisGroup.x = location.x - rect.x; thisGroup.y = location.y - rect.y; thisGroup.markRedraw(); } private _getViewExtent() { return [0, this._size[0]]; } private _renderBackground() { const dataZoomModel = this.dataZoomModel; const size = this._size; const barGroup = this._displayables.sliderGroup; const brushSelect = dataZoomModel.get('brushSelect'); barGroup.add(new Rect({ silent: true, shape: { x: 0, y: 0, width: size[0], height: size[1] }, style: { fill: dataZoomModel.get('backgroundColor') }, z2: -40 })); // Click panel, over shadow, below handles. const clickPanel = new Rect({ shape: { x: 0, y: 0, width: size[0], height: size[1] }, style: { fill: 'transparent' }, z2: 0, onclick: bind(this._onClickPanel, this) }); const zr = this.api.getZr(); if (brushSelect) { clickPanel.on('mousedown', this._onBrushStart, this); clickPanel.cursor = 'crosshair'; zr.on('mousemove', this._onBrush); zr.on('mouseup', this._onBrushEnd); } else { zr.off('mousemove', this._onBrush); zr.off('mouseup', this._onBrushEnd); } barGroup.add(clickPanel); } private _renderDataShadow() { const info = this._dataShadowInfo = this._prepareDataShadowInfo(); this._displayables.dataShadowSegs = []; if (!info) { return; } const size = this._size; const oldSize = this._shadowSize || []; const seriesModel = info.series; const data = seriesModel.getRawData(); const candlestickDim = seriesModel.getShadowDim && seriesModel.getShadowDim(); const otherDim: string = candlestickDim && data.getDimensionInfo(candlestickDim) ? seriesModel.getShadowDim() // @see candlestick : info.otherDim; if (otherDim == null) { return; } let polygonPts = this._shadowPolygonPts; let polylinePts = this._shadowPolylinePts; // Not re-render if data doesn't change. if ( data !== this._shadowData || otherDim !== this._shadowDim || size[0] !== oldSize[0] || size[1] !== oldSize[1] ) { let otherDataExtent = data.getDataExtent(otherDim); // Nice extent. const otherOffset = (otherDataExtent[1] - otherDataExtent[0]) * 0.3; otherDataExtent = [ otherDataExtent[0] - otherOffset, otherDataExtent[1] + otherOffset ]; const otherShadowExtent = [0, size[1]]; const thisShadowExtent = [0, size[0]]; const areaPoints = [[size[0], 0], [0, 0]]; const linePoints: number[][] = []; const step = thisShadowExtent[1] / (data.count() - 1); let thisCoord = 0; // Optimize for large data shadow const stride = Math.round(data.count() / size[0]); let lastIsEmpty: boolean; data.each([otherDim], function (value: ParsedValue, index) { if (stride > 0 && (index % stride)) { thisCoord += step; return; } // FIXME // Should consider axis.min/axis.max when drawing dataShadow. // FIXME // 应该使用统一的空判断?还是在list里进行空判断? const isEmpty = value == null || isNaN(value as number) || value === ''; // See #4235. const otherCoord = isEmpty ? 0 : linearMap(value as number, otherDataExtent, otherShadowExtent, true); // Attempt to draw data shadow precisely when there are empty value. if (isEmpty && !lastIsEmpty && index) { areaPoints.push([areaPoints[areaPoints.length - 1][0], 0]); linePoints.push([linePoints[linePoints.length - 1][0], 0]); } else if (!isEmpty && lastIsEmpty) { areaPoints.push([thisCoord, 0]); linePoints.push([thisCoord, 0]); } areaPoints.push([thisCoord, otherCoord]); linePoints.push([thisCoord, otherCoord]); thisCoord += step; lastIsEmpty = isEmpty; }); polygonPts = this._shadowPolygonPts = areaPoints; polylinePts = this._shadowPolylinePts = linePoints; } this._shadowData = data; this._shadowDim = otherDim; this._shadowSize = [size[0], size[1]]; const dataZoomModel = this.dataZoomModel; function createDataShadowGroup(isSelectedArea?: boolean) { const model = dataZoomModel.getModel(isSelectedArea ? 'selectedDataBackground' : 'dataBackground'); const group = new graphic.Group(); const polygon = new graphic.Polygon({ shape: {points: polygonPts}, segmentIgnoreThreshold: 1, style: model.getModel('areaStyle').getAreaStyle(), silent: true, z2: -20 }); const polyline = new graphic.Polyline({ shape: {points: polylinePts}, segmentIgnoreThreshold: 1, style: model.getModel('lineStyle').getLineStyle(), silent: true, z2: -19 }); group.add(polygon); group.add(polyline); return group; } // let dataBackgroundModel = dataZoomModel.getModel('dataBackground'); for (let i = 0; i < 3; i++) { const group = createDataShadowGroup(i === 1); this._displayables.sliderGroup.add(group); this._displayables.dataShadowSegs.push(group); } } private _prepareDataShadowInfo() { const dataZoomModel = this.dataZoomModel; const showDataShadow = dataZoomModel.get('showDataShadow'); if (showDataShadow === false) { return; } // Find a representative series. let result: SliderZoomView['_dataShadowInfo']; const ecModel = this.ecModel; dataZoomModel.eachTargetAxis(function (axisDim, axisIndex) { const seriesModels = dataZoomModel .getAxisProxy(axisDim, axisIndex) .getTargetSeriesModels(); each(seriesModels, function (seriesModel) { if (result) { return; } if (showDataShadow !== true && indexOf( SHOW_DATA_SHADOW_SERIES_TYPE, seriesModel.get('type') ) < 0 ) { return; } const thisAxis = ( ecModel.getComponent(getAxisMainType(axisDim), axisIndex) as AxisBaseModel ).axis; let otherDim = getOtherDim(axisDim); let otherAxisInverse; const coordSys = seriesModel.coordinateSystem; if (otherDim != null && coordSys.getOtherAxis) { otherAxisInverse = coordSys.getOtherAxis(thisAxis).inverse; } otherDim = seriesModel.getData().mapDimension(otherDim); result = { thisAxis: thisAxis, series: seriesModel, thisDim: axisDim, otherDim: otherDim, otherAxisInverse: otherAxisInverse }; }, this); }, this); return result; } private _renderHandle() { const thisGroup = this.group; const displayables = this._displayables; const handles: [graphic.Path, graphic.Path] = displayables.handles = [null, null]; const handleLabels: [graphic.Text, graphic.Text] = displayables.handleLabels = [null, null]; const sliderGroup = this._displayables.sliderGroup; const size = this._size; const dataZoomModel = this.dataZoomModel; const api = this.api; const borderRadius = dataZoomModel.get('borderRadius') || 0; const brushSelect = dataZoomModel.get('brushSelect'); const filler = displayables.filler = new Rect({ silent: brushSelect, style: { fill: dataZoomModel.get('fillerColor') }, textConfig: { position: 'inside' } }); sliderGroup.add(filler); // Frame border. sliderGroup.add(new Rect({ silent: true, subPixelOptimize: true, shape: { x: 0, y: 0, width: size[0], height: size[1], r: borderRadius }, style: { // deprecated option stroke: dataZoomModel.get('dataBackgroundColor' as any) || dataZoomModel.get('borderColor'), lineWidth: DEFAULT_FRAME_BORDER_WIDTH, fill: 'rgba(0,0,0,0)' } })); // Left and right handle to resize each([0, 1] as const, function (handleIndex) { let iconStr = dataZoomModel.get('handleIcon'); if ( !symbolBuildProxies[iconStr] && iconStr.indexOf('path://') < 0 && iconStr.indexOf('image://') < 0 ) { // Compatitable with the old icon parsers. Which can use a path string without path:// iconStr = 'path://' + iconStr; if (__DEV__) { deprecateLog('handleIcon now needs \'path://\' prefix when using a path string'); } } const path = createSymbol( iconStr, -1, 0, 2, 2, null, true ) as graphic.Path; path.attr({ cursor: getCursor(this._orient), draggable: true, drift: bind(this._onDragMove, this, handleIndex), ondragend: bind(this._onDragEnd, this), onmouseover: bind(this._showDataInfo, this, true), onmouseout: bind(this._showDataInfo, this, false), z2: 5 }); const bRect = path.getBoundingRect(); const handleSize = dataZoomModel.get('handleSize'); this._handleHeight = parsePercent(handleSize, this._size[1]); this._handleWidth = bRect.width / bRect.height * this._handleHeight; path.setStyle(dataZoomModel.getModel('handleStyle').getItemStyle()); path.style.strokeNoScale = true; path.rectHover = true; path.ensureState('emphasis').style = dataZoomModel.getModel(['emphasis', 'handleStyle']).getItemStyle(); enableHoverEmphasis(path); const handleColor = dataZoomModel.get('handleColor' as any); // deprecated option // Compatitable with previous version if (handleColor != null) { path.style.fill = handleColor; } sliderGroup.add(handles[handleIndex] = path); const textStyleModel = dataZoomModel.getModel('textStyle'); thisGroup.add( handleLabels[handleIndex] = new graphic.Text({ silent: true, invisible: true, style: createTextStyle(textStyleModel, { x: 0, y: 0, text: '', verticalAlign: 'middle', align: 'center', fill: textStyleModel.getTextColor(), font: textStyleModel.getFont() }), z2: 10 })); }, this); // Handle to move. Only visible when brushSelect is set true. let actualMoveZone: Displayable = filler; if (brushSelect) { const moveHandleHeight = parsePercent(dataZoomModel.get('moveHandleSize'), size[1]); const moveHandle = displayables.moveHandle = new graphic.Rect({ style: dataZoomModel.getModel('moveHandleStyle').getItemStyle(), silent: true, shape: { r: [0, 0, 2, 2], y: size[1] - 0.5, height: moveHandleHeight } }); const iconSize = moveHandleHeight * 0.8; const moveHandleIcon = displayables.moveHandleIcon = createSymbol( dataZoomModel.get('moveHandleIcon'), -iconSize / 2, -iconSize / 2, iconSize, iconSize, '#fff', true ); moveHandleIcon.silent = true; moveHandleIcon.y = size[1] + moveHandleHeight / 2 - 0.5; moveHandle.ensureState('emphasis').style = dataZoomModel.getModel( ['emphasis', 'moveHandleStyle'] ).getItemStyle(); const moveZoneExpandSize = Math.min(size[1] / 2, Math.max(moveHandleHeight, 10)); actualMoveZone = displayables.moveZone = new graphic.Rect({ invisible: true, shape: { y: size[1] - moveZoneExpandSize, height: moveHandleHeight + moveZoneExpandSize } }); actualMoveZone.on('mouseover', () => { api.enterEmphasis(moveHandle); }) .on('mouseout', () => { api.leaveEmphasis(moveHandle); }); sliderGroup.add(moveHandle); sliderGroup.add(moveHandleIcon); sliderGroup.add(actualMoveZone); } actualMoveZone.attr({ draggable: true, cursor: getCursor(this._orient), drift: bind(this._onDragMove, this, 'all'), ondragstart: bind(this._showDataInfo, this, true), ondragend: bind(this._onDragEnd, this), onmouseover: bind(this._showDataInfo, this, true), onmouseout: bind(this._showDataInfo, this, false) }); } private _resetInterval() { const range = this._range = this.dataZoomModel.getPercentRange(); const viewExtent = this._getViewExtent(); this._handleEnds = [ linearMap(range[0], [0, 100], viewExtent, true), linearMap(range[1], [0, 100], viewExtent, true) ]; } private _updateInterval(handleIndex: 0 | 1 | 'all', delta: number): boolean { const dataZoomModel = this.dataZoomModel; const handleEnds = this._handleEnds; const viewExtend = this._getViewExtent(); const minMaxSpan = dataZoomModel.findRepresentativeAxisProxy().getMinMaxSpan(); const percentExtent = [0, 100]; sliderMove( delta, handleEnds, viewExtend, dataZoomModel.get('zoomLock') ? 'all' : handleIndex, minMaxSpan.minSpan != null ? linearMap(minMaxSpan.minSpan, percentExtent, viewExtend, true) : null, minMaxSpan.maxSpan != null ? linearMap(minMaxSpan.maxSpan, percentExtent, viewExtend, true) : null ); const lastRange = this._range; const range = this._range = asc([ linearMap(handleEnds[0], viewExtend, percentExtent, true), linearMap(handleEnds[1], viewExtend, percentExtent, true) ]); return !lastRange || lastRange[0] !== range[0] || lastRange[1] !== range[1]; } private _updateView(nonRealtime?: boolean) { const displaybles = this._displayables; const handleEnds = this._handleEnds; const handleInterval = asc(handleEnds.slice()); const size = this._size; each([0, 1] as const, function (handleIndex) { // Handles const handle = displaybles.handles[handleIndex]; const handleHeight = this._handleHeight; (handle as graphic.Path).attr({ scaleX: handleHeight / 2, scaleY: handleHeight / 2, // This is a trick, by adding an extra tiny offset to let the default handle's end point align to the drag window. // NOTE: It may affect some custom shapes a bit. But we prefer to have better result by default. x: handleEnds[handleIndex] + (handleIndex ? -1 : 1), y: size[1] / 2 - handleHeight / 2 }); }, this); // Filler displaybles.filler.setShape({ x: handleInterval[0], y: 0, width: handleInterval[1] - handleInterval[0], height: size[1] }); const viewExtent = { x: handleInterval[0], width: handleInterval[1] - handleInterval[0] }; // Move handle if (displaybles.moveHandle) { displaybles.moveHandle.setShape(viewExtent); displaybles.moveZone.setShape(viewExtent); // Force update path on the invisible object displaybles.moveZone.getBoundingRect(); displaybles.moveHandleIcon && displaybles.moveHandleIcon.attr('x', viewExtent.x + viewExtent.width / 2); } // update clip path of shadow. const dataShadowSegs = displaybles.dataShadowSegs; const segIntervals = [0, handleInterval[0], handleInterval[1], size[0]]; for (let i = 0; i < dataShadowSegs.length; i++) { const segGroup = dataShadowSegs[i]; let clipPath = segGroup.getClipPath(); if (!clipPath) { clipPath = new graphic.Rect(); segGroup.setClipPath(clipPath); } clipPath.setShape({ x: segIntervals[i], y: 0, width: segIntervals[i + 1] - segIntervals[i], height: size[1] }); } this._updateDataInfo(nonRealtime); } private _updateDataInfo(nonRealtime?: boolean) { const dataZoomModel = this.dataZoomModel; const displaybles = this._displayables; const handleLabels = displaybles.handleLabels; const orient = this._orient; let labelTexts = ['', '']; // FIXME // date型,支持formatter,autoformatter(ec2 date.getAutoFormatter) if (dataZoomModel.get('showDetail')) { const axisProxy = dataZoomModel.findRepresentativeAxisProxy(); if (axisProxy) { const axis = axisProxy.getAxisModel().axis; const range = this._range; const dataInterval = nonRealtime // See #4434, data and axis are not processed and reset yet in non-realtime mode. ? axisProxy.calculateDataWindow({ start: range[0], end: range[1] }).valueWindow : axisProxy.getDataValueWindow(); labelTexts = [ this._formatLabel(dataInterval[0], axis), this._formatLabel(dataInterval[1], axis) ]; } } const orderedHandleEnds = asc(this._handleEnds.slice()); setLabel.call(this, 0); setLabel.call(this, 1); function setLabel(this: SliderZoomView, handleIndex: 0 | 1) { // Label // Text should not transform by barGroup. // Ignore handlers transform const barTransform = graphic.getTransform( displaybles.handles[handleIndex].parent, this.group ); const direction = graphic.transformDirection( handleIndex === 0 ? 'right' : 'left', barTransform ); const offset = this._handleWidth / 2 + LABEL_GAP; const textPoint = graphic.applyTransform( [ orderedHandleEnds[handleIndex] + (handleIndex === 0 ? -offset : offset), this._size[1] / 2 ], barTransform ); handleLabels[handleIndex].setStyle({ x: textPoint[0], y: textPoint[1], verticalAlign: orient === HORIZONTAL ? 'middle' : direction as ZRTextVerticalAlign, align: orient === HORIZONTAL ? direction as ZRTextAlign : 'center', text: labelTexts[handleIndex] }); } } private _formatLabel(value: ParsedValue, axis: Axis) { const dataZoomModel = this.dataZoomModel; const labelFormatter = dataZoomModel.get('labelFormatter'); let labelPrecision = dataZoomModel.get('labelPrecision'); if (labelPrecision == null || labelPrecision === 'auto') { labelPrecision = axis.getPixelPrecision(); } const valueStr = (value == null || isNaN(value as number)) ? '' // FIXME Glue code : (axis.type === 'category' || axis.type === 'time') ? axis.scale.getLabel({ value: Math.round(value as number) }) // param of toFixed should less then 20. : (value as number).toFixed(Math.min(labelPrecision as number, 20)); return isFunction(labelFormatter) ? labelFormatter(value as number, valueStr) : isString(labelFormatter) ? labelFormatter.replace('{value}', valueStr) : valueStr; } /** * @param showOrHide true: show, false: hide */ private _showDataInfo(showOrHide?: boolean) { // Always show when drgging. showOrHide = this._dragging || showOrHide; const displayables = this._displayables; const handleLabels = displayables.handleLabels; handleLabels[0].attr('invisible', !showOrHide); handleLabels[1].attr('invisible', !showOrHide); // Highlight move handle displayables.moveHandle && this.api[showOrHide ? 'enterEmphasis' : 'leaveEmphasis'](displayables.moveHandle, 1); } private _onDragMove(handleIndex: 0 | 1 | 'all', dx: number, dy: number, event: ZRElementEvent) { this._dragging = true; // For mobile device, prevent screen slider on the button. eventTool.stop(event.event); // Transform dx, dy to bar coordination. const barTransform = this._displayables.sliderGroup.getLocalTransform(); const vertex = graphic.applyTransform([dx, dy], barTransform, true); const changed = this._updateInterval(handleIndex, vertex[0]); const realtime = this.dataZoomModel.get('realtime'); this._updateView(!realtime); // Avoid dispatch dataZoom repeatly but range not changed, // which cause bad visual effect when progressive enabled. changed && realtime && this._dispatchZoomAction(true); } private _onDragEnd() { this._dragging = false; this._showDataInfo(false); // While in realtime mode and stream mode, dispatch action when // drag end will cause the whole view rerender, which is unnecessary. const realtime = this.dataZoomModel.get('realtime'); !realtime && this._dispatchZoomAction(false); } private _onClickPanel(e: ZRElementEvent) { const size = this._size; const localPoint = this._displayables.sliderGroup.transformCoordToLocal(e.offsetX, e.offsetY); if (localPoint[0] < 0 || localPoint[0] > size[0] || localPoint[1] < 0 || localPoint[1] > size[1] ) { return; } const handleEnds = this._handleEnds; const center = (handleEnds[0] + handleEnds[1]) / 2; const changed = this._updateInterval('all', localPoint[0] - center); this._updateView(); changed && this._dispatchZoomAction(false); } private _onBrushStart(e: ZRElementEvent) { const x = e.offsetX; const y = e.offsetY; this._brushStart = new graphic.Point(x, y); this._brushing = true; this._brushStartTime = +new Date(); // this._updateBrushRect(x, y); } private _onBrushEnd(e: ZRElementEvent) { if (!this._brushing) { return; } const brushRect = this._displayables.brushRect; this._brushing = false; if (!brushRect) { return; } brushRect.attr('ignore', true); const brushShape = brushRect.shape; const brushEndTime = +new Date(); // console.log(brushEndTime - this._brushStartTime); if (brushEndTime - this._brushStartTime < 200 && Math.abs(brushShape.width) < 5) { // Will treat it as a click return; } const viewExtend = this._getViewExtent(); const percentExtent = [0, 100]; this._range = asc([ linearMap(brushShape.x, viewExtend, percentExtent, true), linearMap(brushShape.x + brushShape.width, viewExtend, percentExtent, true) ]); this._handleEnds = [brushShape.x, brushShape.x + brushShape.width]; this._updateView(); this._dispatchZoomAction(false); } private _onBrush(e: ZRElementEvent) { if (this._brushing) { // For mobile device, prevent screen slider on the button. eventTool.stop(e.event); this._updateBrushRect(e.offsetX, e.offsetY); } } private _updateBrushRect(mouseX: number, mouseY: number) { const displayables = this._displayables; const dataZoomModel = this.dataZoomModel; let brushRect = displayables.brushRect; if (!brushRect) { brushRect = displayables.brushRect = new Rect({ silent: true, style: dataZoomModel.getModel('brushStyle').getItemStyle() }); displayables.sliderGroup.add(brushRect); } brushRect.attr('ignore', false); const brushStart = this._brushStart; const sliderGroup = this._displayables.sliderGroup; const endPoint = sliderGroup.transformCoordToLocal(mouseX, mouseY); const startPoint = sliderGroup.transformCoordToLocal(brushStart.x, brushStart.y); const size = this._size; endPoint[0] = Math.max(Math.min(size[0], endPoint[0]), 0); brushRect.setShape({ x: startPoint[0], y: 0, width: endPoint[0] - startPoint[0], height: size[1] }); } /** * This action will be throttled. */ _dispatchZoomAction(realtime: boolean) { const range = this._range; this.api.dispatchAction({ type: 'dataZoom', from: this.uid, dataZoomId: this.dataZoomModel.id, animation: realtime ? REALTIME_ANIMATION_CONFIG : null, start: range[0], end: range[1] }); } private _findCoordRect() { // Find the grid corresponding to the first axis referred by dataZoom. let rect: RectLike; const coordSysInfoList = collectReferCoordSysModelInfo(this.dataZoomModel).infoList; if (!rect && coordSysInfoList.length) { const coordSys = coordSysInfoList[0].model.coordinateSystem; rect = coordSys.getRect && coordSys.getRect(); } if (!rect) { const width = this.api.getWidth(); const height = this.api.getHeight(); rect = { x: width * 0.2, y: height * 0.2, width: width * 0.6, height: height * 0.6 }; } return rect; } } function getOtherDim(thisDim: 'x' | 'y' | 'radius' | 'angle' | 'single' | 'z') { // FIXME // 这个逻辑和getOtherAxis里一致,但是写在这里是否不好 const map = {x: 'y', y: 'x', radius: 'angle', angle: 'radius'}; return map[thisDim as 'x' | 'y' | 'radius' | 'angle']; } function getCursor(orient: LayoutOrient) { return orient === 'vertical' ? 'ns-resize' : 'ew-resize'; } export default SliderZoomView;