/* * 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 BoundingRect, { RectLike } from 'zrender/src/core/BoundingRect'; import * as matrix from 'zrender/src/core/matrix'; import * as graphic from '../../util/graphic'; import { createTextStyle } from '../../label/labelStyle'; import * as layout from '../../util/layout'; import TimelineView from './TimelineView'; import TimelineAxis from './TimelineAxis'; import {createSymbol, normalizeSymbolOffset, normalizeSymbolSize} from '../../util/symbol'; import * as numberUtil from '../../util/number'; import GlobalModel from '../../model/Global'; import ExtensionAPI from '../../core/ExtensionAPI'; import { merge, each, extend, isString, bind, defaults, retrieve2 } from 'zrender/src/core/util'; import SliderTimelineModel from './SliderTimelineModel'; import { LayoutOrient, ZRTextAlign, ZRTextVerticalAlign, ZRElementEvent, ScaleTick } from '../../util/types'; import TimelineModel, { TimelineDataItemOption, TimelineCheckpointStyle } from './TimelineModel'; import { TimelineChangePayload, TimelinePlayChangePayload } from './timelineAction'; import Model from '../../model/Model'; import { PathProps, PathStyleProps } from 'zrender/src/graphic/Path'; import Scale from '../../scale/Scale'; import OrdinalScale from '../../scale/Ordinal'; import TimeScale from '../../scale/Time'; import IntervalScale from '../../scale/Interval'; import { VectorArray } from 'zrender/src/core/vector'; import { parsePercent } from 'zrender/src/contain/text'; import { makeInner } from '../../util/model'; import { getECData } from '../../util/innerStore'; import { enableHoverEmphasis } from '../../util/states'; import { createTooltipMarkup } from '../tooltip/tooltipMarkup'; import Displayable from 'zrender/src/graphic/Displayable'; const PI = Math.PI; type TimelineSymbol = ReturnType; type RenderMethodName = '_renderAxisLine' | '_renderAxisTick' | '_renderControl' | '_renderCurrentPointer'; type ControlName = 'play' | 'stop' | 'next' | 'prev'; type ControlIconName = 'playIcon' | 'stopIcon' | 'nextIcon' | 'prevIcon'; const labelDataIndexStore = makeInner<{ dataIndex: number }, graphic.Text>(); interface LayoutInfo { viewRect: BoundingRect mainLength: number orient: LayoutOrient rotation: number labelRotation: number labelPosOpt: number | '+' | '-' labelAlign: ZRTextAlign labelBaseline: ZRTextVerticalAlign playPosition: number[] prevBtnPosition: number[] nextBtnPosition: number[] axisExtent: number[] controlSize: number controlGap: number } class SliderTimelineView extends TimelineView { static type = 'timeline.slider'; type = SliderTimelineView.type; api: ExtensionAPI; model: SliderTimelineModel; ecModel: GlobalModel; private _axis: TimelineAxis; private _viewRect: BoundingRect; private _timer: number; private _currentPointer: TimelineSymbol; private _progressLine: graphic.Line; private _mainGroup: graphic.Group; private _labelGroup: graphic.Group; private _tickSymbols: graphic.Path[]; private _tickLabels: graphic.Text[]; init(ecModel: GlobalModel, api: ExtensionAPI) { this.api = api; } /** * @override */ render(timelineModel: SliderTimelineModel, ecModel: GlobalModel, api: ExtensionAPI) { this.model = timelineModel; this.api = api; this.ecModel = ecModel; this.group.removeAll(); if (timelineModel.get('show', true)) { const layoutInfo = this._layout(timelineModel, api); const mainGroup = this._createGroup('_mainGroup'); const labelGroup = this._createGroup('_labelGroup'); const axis = this._axis = this._createAxis(layoutInfo, timelineModel); timelineModel.formatTooltip = function (dataIndex: number) { const name = axis.scale.getLabel({value: dataIndex}); return createTooltipMarkup('nameValue', { noName: true, value: name }); }; each( ['AxisLine', 'AxisTick', 'Control', 'CurrentPointer'] as const, function (name) { this['_render' + name as RenderMethodName](layoutInfo, mainGroup, axis, timelineModel); }, this ); this._renderAxisLabel(layoutInfo, labelGroup, axis, timelineModel); this._position(layoutInfo, timelineModel); } this._doPlayStop(); this._updateTicksStatus(); } /** * @override */ remove() { this._clearTimer(); this.group.removeAll(); } /** * @override */ dispose() { this._clearTimer(); } private _layout(timelineModel: SliderTimelineModel, api: ExtensionAPI): LayoutInfo { const labelPosOpt = timelineModel.get(['label', 'position']); const orient = timelineModel.get('orient'); const viewRect = getViewRect(timelineModel, api); let parsedLabelPos: number | '+' | '-'; // Auto label offset. if (labelPosOpt == null || labelPosOpt === 'auto') { parsedLabelPos = orient === 'horizontal' ? ((viewRect.y + viewRect.height / 2) < api.getHeight() / 2 ? '-' : '+') : ((viewRect.x + viewRect.width / 2) < api.getWidth() / 2 ? '+' : '-'); } else if (isString(labelPosOpt)) { parsedLabelPos = ({ horizontal: {top: '-', bottom: '+'}, vertical: {left: '-', right: '+'} } as const)[orient][labelPosOpt]; } else { // is number parsedLabelPos = labelPosOpt; } const labelAlignMap = { horizontal: 'center', vertical: (parsedLabelPos >= 0 || parsedLabelPos === '+') ? 'left' : 'right' }; const labelBaselineMap = { horizontal: (parsedLabelPos >= 0 || parsedLabelPos === '+') ? 'top' : 'bottom', vertical: 'middle' }; const rotationMap = { horizontal: 0, vertical: PI / 2 }; // Position const mainLength = orient === 'vertical' ? viewRect.height : viewRect.width; const controlModel = timelineModel.getModel('controlStyle'); const showControl = controlModel.get('show', true); const controlSize = showControl ? controlModel.get('itemSize') : 0; const controlGap = showControl ? controlModel.get('itemGap') : 0; const sizePlusGap = controlSize + controlGap; // Special label rotate. let labelRotation = timelineModel.get(['label', 'rotate']) || 0; labelRotation = labelRotation * PI / 180; // To radian. let playPosition: number[]; let prevBtnPosition: number[]; let nextBtnPosition: number[]; const controlPosition = controlModel.get('position', true); const showPlayBtn = showControl && controlModel.get('showPlayBtn', true); const showPrevBtn = showControl && controlModel.get('showPrevBtn', true); const showNextBtn = showControl && controlModel.get('showNextBtn', true); let xLeft = 0; let xRight = mainLength; // position[0] means left, position[1] means middle. if (controlPosition === 'left' || controlPosition === 'bottom') { showPlayBtn && (playPosition = [0, 0], xLeft += sizePlusGap); showPrevBtn && (prevBtnPosition = [xLeft, 0], xLeft += sizePlusGap); showNextBtn && (nextBtnPosition = [xRight - controlSize, 0], xRight -= sizePlusGap); } else { // 'top' 'right' showPlayBtn && (playPosition = [xRight - controlSize, 0], xRight -= sizePlusGap); showPrevBtn && (prevBtnPosition = [0, 0], xLeft += sizePlusGap); showNextBtn && (nextBtnPosition = [xRight - controlSize, 0], xRight -= sizePlusGap); } const axisExtent = [xLeft, xRight]; if (timelineModel.get('inverse')) { axisExtent.reverse(); } return { viewRect: viewRect, mainLength: mainLength, orient: orient, rotation: rotationMap[orient], labelRotation: labelRotation, labelPosOpt: parsedLabelPos, labelAlign: timelineModel.get(['label', 'align']) || labelAlignMap[orient] as ZRTextAlign, labelBaseline: timelineModel.get(['label', 'verticalAlign']) || timelineModel.get(['label', 'baseline']) || labelBaselineMap[orient] as ZRTextVerticalAlign, // Based on mainGroup. playPosition: playPosition, prevBtnPosition: prevBtnPosition, nextBtnPosition: nextBtnPosition, axisExtent: axisExtent, controlSize: controlSize, controlGap: controlGap }; } private _position(layoutInfo: LayoutInfo, timelineModel: SliderTimelineModel) { // Position is be called finally, because bounding rect is needed for // adapt content to fill viewRect (auto adapt offset). // Timeline may be not all in the viewRect when 'offset' is specified // as a number, because it is more appropriate that label aligns at // 'offset' but not the other edge defined by viewRect. const mainGroup = this._mainGroup; const labelGroup = this._labelGroup; let viewRect = layoutInfo.viewRect; if (layoutInfo.orient === 'vertical') { // transform to horizontal, inverse rotate by left-top point. const m = matrix.create(); const rotateOriginX = viewRect.x; const rotateOriginY = viewRect.y + viewRect.height; matrix.translate(m, m, [-rotateOriginX, -rotateOriginY]); matrix.rotate(m, m, -PI / 2); matrix.translate(m, m, [rotateOriginX, rotateOriginY]); viewRect = viewRect.clone(); viewRect.applyTransform(m); } const viewBound = getBound(viewRect); const mainBound = getBound(mainGroup.getBoundingRect()); const labelBound = getBound(labelGroup.getBoundingRect()); const mainPosition = [mainGroup.x, mainGroup.y]; const labelsPosition = [labelGroup.x, labelGroup.y]; labelsPosition[0] = mainPosition[0] = viewBound[0][0]; const labelPosOpt = layoutInfo.labelPosOpt; if (labelPosOpt == null || isString(labelPosOpt)) { // '+' or '-' const mainBoundIdx = labelPosOpt === '+' ? 0 : 1; toBound(mainPosition, mainBound, viewBound, 1, mainBoundIdx); toBound(labelsPosition, labelBound, viewBound, 1, 1 - mainBoundIdx); } else { const mainBoundIdx = labelPosOpt >= 0 ? 0 : 1; toBound(mainPosition, mainBound, viewBound, 1, mainBoundIdx); labelsPosition[1] = mainPosition[1] + labelPosOpt; } mainGroup.setPosition(mainPosition); labelGroup.setPosition(labelsPosition); mainGroup.rotation = labelGroup.rotation = layoutInfo.rotation; setOrigin(mainGroup); setOrigin(labelGroup); function setOrigin(targetGroup: graphic.Group) { targetGroup.originX = viewBound[0][0] - targetGroup.x; targetGroup.originY = viewBound[1][0] - targetGroup.y; } function getBound(rect: RectLike) { // [[xmin, xmax], [ymin, ymax]] return [ [rect.x, rect.x + rect.width], [rect.y, rect.y + rect.height] ]; } function toBound(fromPos: VectorArray, from: number[][], to: number[][], dimIdx: number, boundIdx: number) { fromPos[dimIdx] += to[dimIdx][boundIdx] - from[dimIdx][boundIdx]; } } private _createAxis(layoutInfo: LayoutInfo, timelineModel: SliderTimelineModel) { const data = timelineModel.getData(); const axisType = timelineModel.get('axisType'); const scale = createScaleByModel(timelineModel, axisType); // Customize scale. The `tickValue` is `dataIndex`. scale.getTicks = function () { return data.mapArray(['value'], function (value: number) { return {value}; }); }; const dataExtent = data.getDataExtent('value'); scale.setExtent(dataExtent[0], dataExtent[1]); scale.calcNiceTicks(); const axis = new TimelineAxis('value', scale, layoutInfo.axisExtent as [number, number], axisType); axis.model = timelineModel; return axis; } private _createGroup(key: '_mainGroup' | '_labelGroup') { const newGroup = this[key] = new graphic.Group(); this.group.add(newGroup); return newGroup; } private _renderAxisLine( layoutInfo: LayoutInfo, group: graphic.Group, axis: TimelineAxis, timelineModel: SliderTimelineModel ) { const axisExtent = axis.getExtent(); if (!timelineModel.get(['lineStyle', 'show'])) { return; } const line = new graphic.Line({ shape: { x1: axisExtent[0], y1: 0, x2: axisExtent[1], y2: 0 }, style: extend( {lineCap: 'round'}, timelineModel.getModel('lineStyle').getLineStyle() ), silent: true, z2: 1 }); group.add(line); const progressLine = this._progressLine = new graphic.Line({ shape: { x1: axisExtent[0], x2: this._currentPointer ? this._currentPointer.x : axisExtent[0], y1: 0, y2: 0 }, style: defaults( { lineCap: 'round', lineWidth: line.style.lineWidth } as PathStyleProps, timelineModel.getModel(['progress', 'lineStyle']).getLineStyle() ), silent: true, z2: 1 }); group.add(progressLine); } private _renderAxisTick( layoutInfo: LayoutInfo, group: graphic.Group, axis: TimelineAxis, timelineModel: SliderTimelineModel ) { const data = timelineModel.getData(); // Show all ticks, despite ignoring strategy. const ticks = axis.scale.getTicks(); this._tickSymbols = []; // The value is dataIndex, see the customized scale. each(ticks, (tick: ScaleTick) => { const tickCoord = axis.dataToCoord(tick.value); const itemModel = data.getItemModel(tick.value); const itemStyleModel = itemModel.getModel('itemStyle'); const hoverStyleModel = itemModel.getModel(['emphasis', 'itemStyle']); const progressStyleModel = itemModel.getModel(['progress', 'itemStyle']); const symbolOpt = { x: tickCoord, y: 0, onclick: bind(this._changeTimeline, this, tick.value) }; const el = giveSymbol(itemModel, itemStyleModel, group, symbolOpt); el.ensureState('emphasis').style = hoverStyleModel.getItemStyle(); el.ensureState('progress').style = progressStyleModel.getItemStyle(); enableHoverEmphasis(el); const ecData = getECData(el); if (itemModel.get('tooltip')) { ecData.dataIndex = tick.value; ecData.dataModel = timelineModel; } else { ecData.dataIndex = ecData.dataModel = null; } this._tickSymbols.push(el); }); } private _renderAxisLabel( layoutInfo: LayoutInfo, group: graphic.Group, axis: TimelineAxis, timelineModel: SliderTimelineModel ) { const labelModel = axis.getLabelModel(); if (!labelModel.get('show')) { return; } const data = timelineModel.getData(); const labels = axis.getViewLabels(); this._tickLabels = []; each(labels, (labelItem) => { // The tickValue is dataIndex, see the customized scale. const dataIndex = labelItem.tickValue; const itemModel = data.getItemModel(dataIndex); const normalLabelModel = itemModel.getModel('label'); const hoverLabelModel = itemModel.getModel(['emphasis', 'label']); const progressLabelModel = itemModel.getModel(['progress', 'label']); const tickCoord = axis.dataToCoord(labelItem.tickValue); const textEl = new graphic.Text({ x: tickCoord, y: 0, rotation: layoutInfo.labelRotation - layoutInfo.rotation, onclick: bind(this._changeTimeline, this, dataIndex), silent: false, style: createTextStyle(normalLabelModel, { text: labelItem.formattedLabel, align: layoutInfo.labelAlign, verticalAlign: layoutInfo.labelBaseline }) }); textEl.ensureState('emphasis').style = createTextStyle(hoverLabelModel); textEl.ensureState('progress').style = createTextStyle(progressLabelModel); group.add(textEl); enableHoverEmphasis(textEl); labelDataIndexStore(textEl).dataIndex = dataIndex; this._tickLabels.push(textEl); }); } private _renderControl( layoutInfo: LayoutInfo, group: graphic.Group, axis: TimelineAxis, timelineModel: SliderTimelineModel ) { const controlSize = layoutInfo.controlSize; const rotation = layoutInfo.rotation; const itemStyle = timelineModel.getModel('controlStyle').getItemStyle(); const hoverStyle = timelineModel.getModel(['emphasis', 'controlStyle']).getItemStyle(); const playState = timelineModel.getPlayState(); const inverse = timelineModel.get('inverse', true); makeBtn( layoutInfo.nextBtnPosition, 'next', bind(this._changeTimeline, this, inverse ? '-' : '+') ); makeBtn( layoutInfo.prevBtnPosition, 'prev', bind(this._changeTimeline, this, inverse ? '+' : '-') ); makeBtn( layoutInfo.playPosition, (playState ? 'stop' : 'play'), bind(this._handlePlayClick, this, !playState), true ); function makeBtn( position: number[], iconName: ControlName, onclick: () => void, willRotate?: boolean ) { if (!position) { return; } const iconSize = parsePercent( retrieve2(timelineModel.get(['controlStyle', iconName + 'BtnSize' as any]), controlSize), controlSize ); const rect = [0, -iconSize / 2, iconSize, iconSize]; const btn = makeControlIcon(timelineModel, iconName + 'Icon' as ControlIconName, rect, { x: position[0], y: position[1], originX: controlSize / 2, originY: 0, rotation: willRotate ? -rotation : 0, rectHover: true, style: itemStyle, onclick: onclick }); btn.ensureState('emphasis').style = hoverStyle; group.add(btn); enableHoverEmphasis(btn); } } private _renderCurrentPointer( layoutInfo: LayoutInfo, group: graphic.Group, axis: TimelineAxis, timelineModel: SliderTimelineModel ) { const data = timelineModel.getData(); const currentIndex = timelineModel.getCurrentIndex(); const pointerModel = data.getItemModel(currentIndex) .getModel('checkpointStyle'); const me = this; const callback = { onCreate(pointer: TimelineSymbol) { pointer.draggable = true; pointer.drift = bind(me._handlePointerDrag, me); pointer.ondragend = bind(me._handlePointerDragend, me); pointerMoveTo(pointer, me._progressLine, currentIndex, axis, timelineModel, true); }, onUpdate(pointer: TimelineSymbol) { pointerMoveTo(pointer, me._progressLine, currentIndex, axis, timelineModel); } }; // Reuse when exists, for animation and drag. this._currentPointer = giveSymbol( pointerModel, pointerModel, this._mainGroup, {}, this._currentPointer, callback ); } private _handlePlayClick(nextState: boolean) { this._clearTimer(); this.api.dispatchAction({ type: 'timelinePlayChange', playState: nextState, from: this.uid } as TimelinePlayChangePayload); } private _handlePointerDrag(dx: number, dy: number, e: ZRElementEvent) { this._clearTimer(); this._pointerChangeTimeline([e.offsetX, e.offsetY]); } private _handlePointerDragend(e: ZRElementEvent) { this._pointerChangeTimeline([e.offsetX, e.offsetY], true); } private _pointerChangeTimeline(mousePos: number[], trigger?: boolean) { let toCoord = this._toAxisCoord(mousePos)[0]; const axis = this._axis; const axisExtent = numberUtil.asc(axis.getExtent().slice()); toCoord > axisExtent[1] && (toCoord = axisExtent[1]); toCoord < axisExtent[0] && (toCoord = axisExtent[0]); this._currentPointer.x = toCoord; this._currentPointer.markRedraw(); const progressLine = this._progressLine; if (progressLine) { progressLine.shape.x2 = toCoord; progressLine.dirty(); } const targetDataIndex = this._findNearestTick(toCoord); const timelineModel = this.model; if (trigger || ( targetDataIndex !== timelineModel.getCurrentIndex() && timelineModel.get('realtime') )) { this._changeTimeline(targetDataIndex); } } private _doPlayStop() { this._clearTimer(); if (this.model.getPlayState()) { this._timer = setTimeout( () => { // Do not cache const timelineModel = this.model; this._changeTimeline( timelineModel.getCurrentIndex() + (timelineModel.get('rewind', true) ? -1 : 1) ); }, this.model.get('playInterval') ) as any; } } private _toAxisCoord(vertex: number[]) { const trans = this._mainGroup.getLocalTransform(); return graphic.applyTransform(vertex, trans, true); } private _findNearestTick(axisCoord: number) { const data = this.model.getData(); let dist = Infinity; let targetDataIndex; const axis = this._axis; data.each(['value'], function (value, dataIndex) { const coord = axis.dataToCoord(value); const d = Math.abs(coord - axisCoord); if (d < dist) { dist = d; targetDataIndex = dataIndex; } }); return targetDataIndex; } private _clearTimer() { if (this._timer) { clearTimeout(this._timer); this._timer = null; } } private _changeTimeline(nextIndex: number | '+' | '-') { const currentIndex = this.model.getCurrentIndex(); if (nextIndex === '+') { nextIndex = currentIndex + 1; } else if (nextIndex === '-') { nextIndex = currentIndex - 1; } this.api.dispatchAction({ type: 'timelineChange', currentIndex: nextIndex, from: this.uid } as TimelineChangePayload); } private _updateTicksStatus() { const currentIndex = this.model.getCurrentIndex(); const tickSymbols = this._tickSymbols; const tickLabels = this._tickLabels; if (tickSymbols) { for (let i = 0; i < tickSymbols.length; i++) { tickSymbols && tickSymbols[i] && tickSymbols[i].toggleState('progress', i < currentIndex); } } if (tickLabels) { for (let i = 0; i < tickLabels.length; i++) { tickLabels && tickLabels[i] && tickLabels[i].toggleState( 'progress', labelDataIndexStore(tickLabels[i]).dataIndex <= currentIndex ); } } } } function createScaleByModel(model: SliderTimelineModel, axisType?: string): Scale { axisType = axisType || model.get('type'); if (axisType) { switch (axisType) { // Buildin scale case 'category': return new OrdinalScale({ ordinalMeta: model.getCategories(), extent: [Infinity, -Infinity] }); case 'time': return new TimeScale({ locale: model.ecModel.getLocaleModel(), useUTC: model.ecModel.get('useUTC') }); default: // default to be value return new IntervalScale(); } } } function getViewRect(model: SliderTimelineModel, api: ExtensionAPI) { return layout.getLayoutRect( model.getBoxLayoutParams(), { width: api.getWidth(), height: api.getHeight() }, model.get('padding') ); } function makeControlIcon( timelineModel: TimelineModel, objPath: ControlIconName, rect: number[], opts: PathProps ) { const style = opts.style; const icon = graphic.createIcon( timelineModel.get(['controlStyle', objPath]), opts || {}, new BoundingRect(rect[0], rect[1], rect[2], rect[3]) ); // TODO createIcon won't use style in opt. if (style) { (icon as Displayable).setStyle(style); } return icon; } /** * Create symbol or update symbol * opt: basic position and event handlers */ function giveSymbol( hostModel: Model, itemStyleModel: Model, group: graphic.Group, opt: PathProps, symbol?: TimelineSymbol, callback?: { onCreate?: (symbol: TimelineSymbol) => void onUpdate?: (symbol: TimelineSymbol) => void } ) { const color = itemStyleModel.get('color'); if (!symbol) { const symbolType = hostModel.get('symbol'); symbol = createSymbol(symbolType, -1, -1, 2, 2, color); symbol.setStyle('strokeNoScale', true); group.add(symbol); callback && callback.onCreate(symbol); } else { symbol.setColor(color); group.add(symbol); // Group may be new, also need to add. callback && callback.onUpdate(symbol); } // Style const itemStyle = itemStyleModel.getItemStyle(['color']); symbol.setStyle(itemStyle); // Transform and events. opt = merge({ rectHover: true, z2: 100 }, opt, true); const symbolSize = normalizeSymbolSize(hostModel.get('symbolSize')); opt.scaleX = symbolSize[0] / 2; opt.scaleY = symbolSize[1] / 2; const symbolOffset = normalizeSymbolOffset(hostModel.get('symbolOffset'), symbolSize); if (symbolOffset) { opt.x = (opt.x || 0) + symbolOffset[0]; opt.y = (opt.y || 0) + symbolOffset[1]; } const symbolRotate = hostModel.get('symbolRotate'); opt.rotation = (symbolRotate || 0) * Math.PI / 180 || 0; symbol.attr(opt); // FIXME // (1) When symbol.style.strokeNoScale is true and updateTransform is not performed, // getBoundingRect will return wrong result. // (This is supposed to be resolved in zrender, but it is a little difficult to // leverage performance and auto updateTransform) // (2) All of ancesters of symbol do not scale, so we can just updateTransform symbol. symbol.updateTransform(); return symbol; } function pointerMoveTo( pointer: TimelineSymbol, progressLine: graphic.Line, dataIndex: number, axis: TimelineAxis, timelineModel: SliderTimelineModel, noAnimation?: boolean ) { if (pointer.dragging) { return; } const pointerModel = timelineModel.getModel('checkpointStyle'); const toCoord = axis.dataToCoord(timelineModel.getData().get('value', dataIndex)); if (noAnimation || !pointerModel.get('animation', true)) { pointer.attr({ x: toCoord, y: 0 }); progressLine && progressLine.attr({ shape: { x2: toCoord } }); } else { const animationCfg = { duration: pointerModel.get('animationDuration', true), easing: pointerModel.get('animationEasing', true) }; pointer.stopAnimation(null, true); pointer.animateTo({ x: toCoord, y: 0 }, animationCfg); progressLine && progressLine.animateTo({ shape: { x2: toCoord } }, animationCfg); } } export default SliderTimelineView;