| /* |
| * 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} 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 ComponentView from '../../view/Component'; |
| 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.niceTicks(); |
| |
| 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 costomized 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 costomized 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 opt = { |
| position: position, |
| origin: [controlSize / 2, 0], |
| rotation: willRotate ? -rotation : 0, |
| rectHover: true, |
| style: itemStyle, |
| onclick: onclick |
| }; |
| const btn = makeControlIcon(timelineModel, iconName + 'Icon' as ControlIconName, rect, opt); |
| 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(); |
| |
| this._progressLine.shape.x2 = toCoord; |
| this._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); |
| |
| let symbolSize = hostModel.get('symbolSize'); |
| symbolSize = symbolSize instanceof Array |
| ? symbolSize.slice() |
| : [+symbolSize, +symbolSize]; |
| |
| opt.scaleX = symbolSize[0] / 2; |
| opt.scaleY = symbolSize[1] / 2; |
| |
| const symbolOffset = hostModel.get('symbolOffset'); |
| if (symbolOffset) { |
| opt.x = opt.x || 0; |
| opt.y = opt.y || 0; |
| opt.x += numberUtil.parsePercent(symbolOffset[0], symbolSize[0]); |
| opt.y += numberUtil.parsePercent(symbolOffset[1], symbolSize[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; |