Spaces:
Running
Running
| /* | |
| * 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<typeof createSymbol>; | |
| 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<TimelineDataItemOption>(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<TimelineDataItemOption>(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<TimelineDataItemOption>(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<TimelineDataItemOption | TimelineCheckpointStyle>, | |
| itemStyleModel: Model<TimelineDataItemOption['itemStyle'] | TimelineCheckpointStyle>, | |
| 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; | |