From 459a9d8336da7efcd132dc892cf23158cd5ad1f5 Mon Sep 17 00:00:00 2001 From: pissang Date: Wed, 22 Jul 2020 21:16:35 +0800 Subject: [PATCH] feat(timeline): optimize timeline style. add progress config --- src/component/timeline/SliderTimelineModel.ts | 46 ++++-- src/component/timeline/SliderTimelineView.ts | 147 ++++++++++++------ src/component/timeline/TimelineModel.ts | 15 ++ src/echarts.ts | 62 ++++---- test/timeline-finance.html | 6 +- 5 files changed, 181 insertions(+), 95 deletions(-) diff --git a/src/component/timeline/SliderTimelineModel.ts b/src/component/timeline/SliderTimelineModel.ts index 2791773b7..e6c175cec 100644 --- a/src/component/timeline/SliderTimelineModel.ts +++ b/src/component/timeline/SliderTimelineModel.ts @@ -48,13 +48,13 @@ class SliderTimelineModel extends TimelineModel { trigger: 'item' // data item may also have tootip attr. }, - symbol: 'emptyCircle', - symbolSize: 10, + symbol: 'circle', + symbolSize: 12, lineStyle: { show: true, width: 2, - color: '#304654' + color: '#A4BED7' }, label: { // 文本标签 position: 'auto', // auto left right top bottom @@ -66,19 +66,24 @@ class SliderTimelineModel extends TimelineModel { rotate: 0, // formatter: null, // 其余属性默认使用全局文本样式,详见TEXTSTYLE - color: '#304654' + color: '#A4BED7' }, itemStyle: { - color: '#304654', + color: '#A4BED7', borderWidth: 1 }, checkpointStyle: { symbol: 'circle', - symbolSize: 13, - color: '#c23531', - borderWidth: 5, - borderColor: 'rgba(194,53,49, 0.5)', + symbolSize: 15, + color: '#316bf3', + borderColor: '#fff', + borderWidth: 2, + shadowBlur: 2, + shadowOffsetX: 1, + shadowOffsetY: 1, + shadowColor: 'rgba(0, 0, 0, 0.3)', + // borderColor: 'rgba(194,53,49, 0.5)', animation: true, animationDuration: 300, animationEasing: 'quinticInOut' @@ -97,28 +102,37 @@ class SliderTimelineModel extends TimelineModel { nextIcon: 'path://M18.6,50.8l22.5-22.5c0.2-0.2,0.3-0.4,0.3-0.7c0-0.3-0.1-0.5-0.3-0.7L18.7,4.4c-0.1-0.1-0.2-0.3-0.2-0.5 c0-0.4,0.3-0.8,0.8-0.8c0.2,0,0.5,0.1,0.6,0.3l23.5,23.5l0,0c0.2,0.2,0.3,0.4,0.3,0.7c0,0.3-0.1,0.5-0.3,0.7l-0.1,0.1L19.7,52 c-0.1,0.1-0.3,0.2-0.5,0.2c-0.4,0-0.8-0.3-0.8-0.8C18.4,51.2,18.5,51,18.6,50.8z', // jshint ignore:line prevIcon: 'path://M43,52.8L20.4,30.3c-0.2-0.2-0.3-0.4-0.3-0.7c0-0.3,0.1-0.5,0.3-0.7L42.9,6.4c0.1-0.1,0.2-0.3,0.2-0.5 c0-0.4-0.3-0.8-0.8-0.8c-0.2,0-0.5,0.1-0.6,0.3L18.3,28.8l0,0c-0.2,0.2-0.3,0.4-0.3,0.7c0,0.3,0.1,0.5,0.3,0.7l0.1,0.1L41.9,54 c0.1,0.1,0.3,0.2,0.5,0.2c0.4,0,0.8-0.3,0.8-0.8C43.2,53.2,43.1,53,43,52.8z', // jshint ignore:line - color: '#304654', - borderColor: '#304654', + color: '#A4BED7', + borderColor: '#A4BED7', borderWidth: 1 }, - emphasis: { label: { show: true, // 其余属性默认使用全局文本样式,详见TEXTSTYLE - color: '#c23531' + color: '#6f778d' }, itemStyle: { - color: '#c23531' + color: '#316BF3' }, controlStyle: { - color: '#c23531', - borderColor: '#c23531', + color: '#316BF3', + borderColor: '#316BF3', borderWidth: 2 } }, + + progress: { + lineStyle: { + color: '#316BF3' + }, + itemStyle: { + color: '#316BF3' + } + }, + data: [] }); diff --git a/src/component/timeline/SliderTimelineView.ts b/src/component/timeline/SliderTimelineView.ts index e0e608339..f12febd15 100644 --- a/src/component/timeline/SliderTimelineView.ts +++ b/src/component/timeline/SliderTimelineView.ts @@ -30,19 +30,20 @@ import * as numberUtil from '../../util/number'; import {encodeHTML} from '../../util/format'; import GlobalModel from '../../model/Global'; import ExtensionAPI from '../../ExtensionAPI'; -import { merge, each, extend, clone, isString, bind } from 'zrender/src/core/util'; +import { merge, each, extend, clone, isString, bind, defaults } from 'zrender/src/core/util'; import SliderTimelineModel from './SliderTimelineModel'; import ComponentView from '../../view/Component'; import { LayoutOrient, ZRTextAlign, ZRTextVerticalAlign, ZRElementEvent } from '../../util/types'; import TimelineModel, { TimelineDataItemOption, TimelineCheckpointStyle } from './TimelineModel'; import { TimelineChangePayload, TimelinePlayChangePayload } from './timelineAction'; import Model from '../../model/Model'; -import { PathProps } from 'zrender/src/graphic/Path'; +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 ZRText from 'zrender/src/graphic/Text'; const PI = Math.PI; @@ -89,10 +90,15 @@ class SliderTimelineView extends TimelineView { 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; @@ -133,6 +139,8 @@ class SliderTimelineView extends TimelineView { } this._doPlayStop(); + + this._updateTicksStatus(); } /** @@ -150,7 +158,7 @@ class SliderTimelineView extends TimelineView { this._clearTimer(); } - _layout(timelineModel: SliderTimelineModel, api: ExtensionAPI): LayoutInfo { + private _layout(timelineModel: SliderTimelineModel, api: ExtensionAPI): LayoutInfo { const labelPosOpt = timelineModel.get(['label', 'position']); const orient = timelineModel.get('orient'); const viewRect = getViewRect(timelineModel, api); @@ -250,7 +258,7 @@ class SliderTimelineView extends TimelineView { }; } - _position(layoutInfo: LayoutInfo, timelineModel: SliderTimelineModel) { + 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). @@ -321,7 +329,7 @@ class SliderTimelineView extends TimelineView { } } - _createAxis(layoutInfo: LayoutInfo, timelineModel: SliderTimelineModel) { + private _createAxis(layoutInfo: LayoutInfo, timelineModel: SliderTimelineModel) { const data = timelineModel.getData(); const axisType = timelineModel.get('axisType'); @@ -344,13 +352,13 @@ class SliderTimelineView extends TimelineView { return axis; } - _createGroup(key: '_mainGroup' | '_labelGroup') { + private _createGroup(key: '_mainGroup' | '_labelGroup') { const newGroup = this[key] = new graphic.Group(); this.group.add(newGroup); return newGroup; } - _renderAxisLine( + private _renderAxisLine( layoutInfo: LayoutInfo, group: graphic.Group, axis: TimelineAxis, @@ -362,7 +370,7 @@ class SliderTimelineView extends TimelineView { return; } - group.add(new graphic.Line({ + const line = new graphic.Line({ shape: { x1: axisExtent[0], y1: 0, x2: axisExtent[1], y2: 0 @@ -373,13 +381,27 @@ class SliderTimelineView extends TimelineView { ), 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( + private _renderAxisTick( layoutInfo: LayoutInfo, group: graphic.Group, axis: TimelineAxis, @@ -389,18 +411,24 @@ class SliderTimelineView extends TimelineView { // Show all ticks, despite ignoring strategy. const ticks = axis.scale.getTicks(); + this._tickSymbols = []; + // The value is dataIndex, see the costomized scale. - each(ticks, function (value) { + each(ticks, (value) => { const tickCoord = axis.dataToCoord(value); const itemModel = data.getItemModel(value); const itemStyleModel = itemModel.getModel('itemStyle'); const hoverStyleModel = itemModel.getModel(['emphasis', 'itemStyle']); + const progressStyleModel = itemModel.getModel(['progress', 'itemStyle']); + const symbolOpt = { position: [tickCoord, 0], onclick: bind(this._changeTimeline, this, value) }; const el = giveSymbol(itemModel, itemStyleModel, group, symbolOpt); el.ensureState('emphasis').style = hoverStyleModel.getItemStyle(); + el.ensureState('progress').style = progressStyleModel.getItemStyle(); + enableHoverEmphasis(el); const ecData = graphic.getECData(el); @@ -412,13 +440,11 @@ class SliderTimelineView extends TimelineView { ecData.dataIndex = ecData.dataModel = null; } - }, this); + this._tickSymbols.push(el); + }); } - /** - * @private - */ - _renderAxisLabel( + private _renderAxisLabel( layoutInfo: LayoutInfo, group: graphic.Group, axis: TimelineAxis, @@ -433,13 +459,17 @@ class SliderTimelineView extends TimelineView { const data = timelineModel.getData(); const labels = axis.getViewLabels(); - each(labels, function (labelItem) { + this._tickLabels = []; + + each(labels, (labelItem) => { // The tickValue is dataIndex, see the costomized 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, @@ -455,17 +485,17 @@ class SliderTimelineView extends TimelineView { }); textEl.ensureState('emphasis').style = createTextStyle(hoverLabelModel); + textEl.ensureState('progress').style = createTextStyle(progressLabelModel); group.add(textEl); enableHoverEmphasis(textEl); - }, this); + this._tickLabels.push(textEl); + + }); } - /** - * @private - */ - _renderControl( + private _renderControl( layoutInfo: LayoutInfo, group: graphic.Group, axis: TimelineAxis, @@ -521,7 +551,7 @@ class SliderTimelineView extends TimelineView { } } - _renderCurrentPointer( + private _renderCurrentPointer( layoutInfo: LayoutInfo, group: graphic.Group, axis: TimelineAxis, @@ -538,10 +568,10 @@ class SliderTimelineView extends TimelineView { pointer.draggable = true; pointer.drift = bind(me._handlePointerDrag, me); pointer.ondragend = bind(me._handlePointerDragend, me); - pointerMoveTo(pointer, currentIndex, axis, timelineModel, true); + pointerMoveTo(pointer, me._progressLine, currentIndex, axis, timelineModel, true); }, onUpdate(pointer: TimelineSymbol) { - pointerMoveTo(pointer, currentIndex, axis, timelineModel); + pointerMoveTo(pointer, me._progressLine, currentIndex, axis, timelineModel); } }; @@ -551,7 +581,7 @@ class SliderTimelineView extends TimelineView { ); } - _handlePlayClick(nextState: boolean) { + private _handlePlayClick(nextState: boolean) { this._clearTimer(); this.api.dispatchAction({ type: 'timelinePlayChange', @@ -560,16 +590,16 @@ class SliderTimelineView extends TimelineView { } as TimelinePlayChangePayload); } - _handlePointerDrag(dx: number, dy: number, e: ZRElementEvent) { + private _handlePointerDrag(dx: number, dy: number, e: ZRElementEvent) { this._clearTimer(); this._pointerChangeTimeline([e.offsetX, e.offsetY]); } - _handlePointerDragend(e: ZRElementEvent) { + private _handlePointerDragend(e: ZRElementEvent) { this._pointerChangeTimeline([e.offsetX, e.offsetY], true); } - _pointerChangeTimeline(mousePos: number[], trigger?: boolean) { + private _pointerChangeTimeline(mousePos: number[], trigger?: boolean) { let toCoord = this._toAxisCoord(mousePos)[0]; const axis = this._axis; @@ -581,6 +611,9 @@ class SliderTimelineView extends TimelineView { this._currentPointer.x = toCoord; this._currentPointer.markRedraw(); + this._progressLine.shape.x2 = toCoord; + this._progressLine.dirty(); + const targetDataIndex = this._findNearestTick(toCoord); const timelineModel = this.model; @@ -592,7 +625,7 @@ class SliderTimelineView extends TimelineView { } } - _doPlayStop() { + private _doPlayStop() { this._clearTimer(); if (this.model.getPlayState()) { @@ -610,12 +643,12 @@ class SliderTimelineView extends TimelineView { } } - _toAxisCoord(vertex: number[]) { + private _toAxisCoord(vertex: number[]) { const trans = this._mainGroup.getLocalTransform(); return graphic.applyTransform(vertex, trans, true); } - _findNearestTick(axisCoord: number) { + private _findNearestTick(axisCoord: number) { const data = this.model.getData(); let dist = Infinity; let targetDataIndex; @@ -633,14 +666,14 @@ class SliderTimelineView extends TimelineView { return targetDataIndex; } - _clearTimer() { + private _clearTimer() { if (this._timer) { clearTimeout(this._timer); this._timer = null; } } - _changeTimeline(nextIndex: number | '+' | '-') { + private _changeTimeline(nextIndex: number | '+' | '-') { const currentIndex = this.model.getCurrentIndex(); if (nextIndex === '+') { @@ -657,6 +690,23 @@ class SliderTimelineView extends TimelineView { } as TimelineChangePayload); } + private _updateTicksStatus() { + const currentIndex = this.model.getCurrentIndex(); + const tickSymbols = this._tickSymbols; + const tickLabels = this._tickLabels; + if (!(tickSymbols || tickLabels)) { + return; + } + + const len = (tickSymbols || tickLabels).length; + + for (let i = 0; i < len; i++) { + tickSymbols && tickSymbols[i] + && tickSymbols[i].toggleState('progress', i <= currentIndex); + tickLabels && tickLabels[i] + && tickLabels[i].toggleState('progress', i <= currentIndex); + } + } } function createScaleByModel(model: SliderTimelineModel, axisType?: string): Scale { @@ -782,6 +832,7 @@ function giveSymbol( function pointerMoveTo( pointer: TimelineSymbol, + progressLine: graphic.Line, dataIndex: number, axis: TimelineAxis, timelineModel: SliderTimelineModel, @@ -795,20 +846,28 @@ function pointerMoveTo( const toCoord = axis.dataToCoord(timelineModel.getData().get('value', dataIndex)); if (noAnimation || !pointerModel.get('animation', true)) { - pointer.x = toCoord; - pointer.y = 0; + 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 - }, { - duration: pointerModel.get('animationDuration', true), - easing: pointerModel.get('animationEasing', true) - }); + }, animationCfg); + progressLine && progressLine.animateTo({ + shape: { x2: toCoord } + }, animationCfg); } - pointer.markRedraw(); } diff --git a/src/component/timeline/TimelineModel.ts b/src/component/timeline/TimelineModel.ts index 385854c11..2ddea11d0 100644 --- a/src/component/timeline/TimelineModel.ts +++ b/src/component/timeline/TimelineModel.ts @@ -84,6 +84,13 @@ export interface TimelineDataItemOption extends SymbolOptionMixin { checkpointStyle?: TimelineCheckpointStyle } + // Style in progress + progress?: { + lineStyle?: TimelineLineStyleOption + itemStyle?: ItemStyleOption + label?: TimelineLabelOption + } + tooltip?: boolean } @@ -139,6 +146,14 @@ export interface TimelineOption extends ComponentOption, BoxLayoutOptionMixin, S label?: TimelineLabelOption } + + // Style in progress + progress?: { + lineStyle?: TimelineLineStyleOption + itemStyle?: ItemStyleOption + label?: TimelineLabelOption + } + data?: (OptionDataValue | TimelineDataItemOption)[] } class TimelineModel extends ComponentModel { diff --git a/src/echarts.ts b/src/echarts.ts index de8aeab49..3e745dfe3 100644 --- a/src/echarts.ts +++ b/src/echarts.ts @@ -1968,45 +1968,50 @@ class ECharts extends Eventful { } ecIns.getZr().storage.traverse(function (el: ECElement) { - const newStates = []; - const oldStates = el.currentStates; - // Not applied on removed elements, it may still in fading. if (graphic.isElementRemoved(el)) { return; } - - // Keep other states. - for (let i = 0; i < oldStates.length; i++) { - const stateName = oldStates[i]; - if (!(stateName === 'emphasis' || stateName === 'blur' || stateName === 'select')) { - newStates.push(stateName); - } - } - - // Only use states when it's exists. - if (el.selected && el.states.select) { - newStates.push('select'); - } - if (el.hoverState === HOVER_STATE_EMPHASIS && el.states.emphasis) { - newStates.push('emphasis'); - } - else if (el.hoverState === HOVER_STATE_BLUR && el.states.blur) { - newStates.push('blur'); - } - el.useStates(newStates); + applyElementStates(el); }); ecIns[STATUS_NEEDS_UPDATE_KEY] = false; }; + function applyElementStates(el: ECElement) { + const newStates = []; + + const oldStates = el.currentStates; + // Keep other states. + for (let i = 0; i < oldStates.length; i++) { + const stateName = oldStates[i]; + if (!(stateName === 'emphasis' || stateName === 'blur' || stateName === 'select')) { + newStates.push(stateName); + } + } + + // Only use states when it's exists. + if (el.selected && el.states.select) { + newStates.push('select'); + } + if (el.hoverState === HOVER_STATE_EMPHASIS && el.states.emphasis) { + newStates.push('emphasis'); + } + else if (el.hoverState === HOVER_STATE_BLUR && el.states.blur) { + newStates.push('blur'); + } + el.useStates(newStates); + } + function updateHoverLayerStatus(ecIns: ECharts, ecModel: GlobalModel): void { const zr = ecIns._zr; const storage = zr.storage; let elCount = 0; storage.traverse(function (el) { - elCount++; + if (!el.isGroup) { + elCount++; + } }); if (elCount > ecModel.get('hoverLayerThreshold') && !env.node) { @@ -2159,14 +2164,7 @@ class ECharts extends Eventful { // The use higlighted and selected flag to toggle states. if (el.__dirty) { - const states = []; - if ((el as ECElement).selected) { - states.push('select'); - } - if ((el as ECElement).hoverState) { - states.push('emphasis'); - } - el.useStates(states); + applyElementStates(el); } } }); diff --git a/test/timeline-finance.html b/test/timeline-finance.html index 29e9c44a0..3006f0126 100644 --- a/test/timeline-finance.html +++ b/test/timeline-finance.html @@ -83,12 +83,12 @@ var option = { axisType: 'category', // realtime: false, // loop: false, - autoPlay: true, + autoPlay: false, // currentIndex: 2, playInterval: 1000, controlStyle: { - showNextBtn: false, - showPrevBtn: false, + showNextBtn: true, + showPrevBtn: true, position: 'left' }, data: [ -- GitLab