/*
|
* 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 { 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 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();
|
|
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;
|