feat: add trend teimebar component

This commit is contained in:
baizn 2020-09-16 16:49:42 +08:00 committed by Moyee
parent 00e70ed33b
commit 426d07e92a
9 changed files with 2233 additions and 227 deletions

View File

@ -74,10 +74,10 @@
"@antv/component": "^0.6.1",
"@antv/dom-util": "^2.0.1",
"@antv/event-emitter": "~0.1.0",
"@antv/g-base": "^0.5.0",
"@antv/g-canvas": "^0.5.0",
"@antv/g-base": "^0.5.1",
"@antv/g-canvas": "^0.5.1",
"@antv/g-math": "^0.1.1",
"@antv/g-svg": "^0.5.0",
"@antv/g-svg": "^0.5.1",
"@antv/hierarchy": "^0.6.2",
"@antv/matrix-util": "^2.0.4",
"@antv/path-util": "^2.0.3",
@ -130,4 +130,4 @@
"webpack-cli": "^3.3.10",
"worker-loader": "^3.0.0"
}
}
}

View File

@ -0,0 +1,368 @@
/**
* @file G
*/
import { IGroup, IShape } from '@antv/g-base';
import { deepMix } from '@antv/util'
import Button from './timeButton';
import { ShapeStyle } from '../../types';
import { Arrow } from '../../shape'
export const TIMELINE_START = 'timelinestart';
export const TIMELINE_CHANGE = 'timelinechange';
export const TIMELINE_END = 'timelineend';
const DEFAULT_RECT_FILL = '#ccc'
const DEFAULT_RECT_STROKE = 'green'
const DEFAULT_PLAYBTN_STYLE = {
fill: '#607889',
stroke: '#607889'
}
const DEFAULT_PREBTN_STYLE = {
fill: '#fff'
}
const DEFAULT_NEXTBTN_STYLE = {
fill: 'green'
}
const DEFAULT_CONTROLLER_CONFIG = {
speed: 2,
loop: false,
fill: '#fff',
stroke: '#fff',
preBtnStyle: {
fill: '#ccc',
stroke: '#ccc'
},
nextBtnStyle: {
fill: '#ccc',
stroke: '#ccc'
},
playBtnStyle: {
fill: '#fff',
stroke: '#ccc'
}
}
export type ControllerCfg = Partial<{
readonly group: IGroup;
readonly x?: number;
readonly y?: number;
readonly width: number;
readonly height: number;
/** 播放速度1 个 tick 花费时间 */
readonly speed?: number;
/** 是否循环播放 */
readonly loop?: boolean;
readonly fill?: string;
readonly stroke?: string;
readonly preBtnStyle?: ShapeStyle;
readonly nextBtnStyle?: ShapeStyle;
readonly playBtnStyle?: ShapeStyle;
}>
/**
*
* https://www.gapminder.org/tools/#$state$time$value=1870&delay:100;;&chart-type=bubbles
*/
export default class ControllerBtn {
/** 是否处于播放状态 */
private isPlay: boolean;
public playButton: Button;
public preStepButton: IShape;
public nextStepButton: IShape;
private checkedIcon: IShape;
private checkedText: IShape;
readonly controllerCfg?: ControllerCfg;
private group: IGroup;
private speedGroup: IGroup;
private toggleGroup: IGroup;
private speedText: IShape;
private speedPoint: IShape;
private speedAxisY: number[];
// 当前播放速度
private currentSpeed: number;
private currentType: 'signle' | 'range';
constructor(cfg: ControllerCfg) {
this.controllerCfg = deepMix(
{},
cfg,
DEFAULT_CONTROLLER_CONFIG
);
this.group = cfg.group
this.speedAxisY = []
this.currentSpeed = this.controllerCfg.speed
this.currentType = 'range'
this.init()
}
public init() {
this.renderPlayButton();
}
/** 获取播放键 marker path */
private getNextMarkerPath(x, y, len) {
return [
['M', x, y - len],
['L', x + len, y],
['L', x, y + len],
];
}
private getPreMarkerPath(x, y, len) {
return [
['M', x, y - len],
['L', x - len, y],
['L', x, y + len],
];
}
private renderPlayButton() {
const { controllerCfg } = this;
const { width, height, x, y,
fill = DEFAULT_RECT_FILL, stroke = DEFAULT_RECT_STROKE,
playBtnStyle = DEFAULT_PLAYBTN_STYLE,
preBtnStyle = DEFAULT_PREBTN_STYLE,
nextBtnStyle = DEFAULT_NEXTBTN_STYLE } = controllerCfg
const r = height / 2 - 5;
const realY = y + 10
// 绘制最外层的矩形包围框
this.group.addShape('rect', {
attrs: {
x,
y: realY,
width,
height,
stroke,
fill
}
})
if (this.playButton) {
this.playButton.update({
x: width / 2,
y: realY,
r,
});
} else {
this.playButton = new Button({
group: this.group,
x: width / 2,
y: realY + r + 5,
r,
isPlay: this.isPlay,
style: playBtnStyle
});
}
// 前进按钮
this.group.addShape('path', {
attrs: {
path: this.getPreMarkerPath(width / 2 - 5 * r, realY + r + 5, r * 0.5),
...preBtnStyle
},
capture: false,
name: 'preStepBtn'
})
this.group.addShape('path', {
attrs: {
path: this.getPreMarkerPath(width / 2 - 4.5 * r, realY + r + 5, r * 0.5),
...preBtnStyle
},
capture: false,
name: 'preStepBtn'
})
// 后退按钮
this.group.addShape('path', {
attrs: {
path: this.getNextMarkerPath(width / 2 + 5 * r, realY + r + 5, r * 0.5),
...nextBtnStyle
},
capture: false,
name: 'nextStepBtn'
})
this.group.addShape('path', {
attrs: {
path: this.getNextMarkerPath(width / 2 + 4.5 * r, realY + r + 5, r * 0.5),
...nextBtnStyle
},
capture: false,
name: 'nextStepBtn'
})
// 调节speed的按钮
this.renderSpeedBtn()
this.renderToggleTime()
this.bindEvent()
}
private renderSpeedBtn() {
const { y, width } = this.controllerCfg
const speedGroup = this.group.addGroup({
name: 'speed-group'
})
this.speedGroup = speedGroup
let count = 2
const speedNum = []
let maxSpeed = 9
// 增加speed刻度
for (let i = 0; i < 5; i++) {
// 灰色刻度
speedGroup.addShape('rect', {
attrs: {
x: width * 4 / 5,
y: y + 10 + i * (i + 1) + count,
width: 15,
height: 2,
fill: '#ccc'
},
speed: maxSpeed,
name: 'speed-rect'
})
this.speedAxisY.push(y + 10 + i * (i + 1) + count)
speedNum.push(maxSpeed)
maxSpeed = maxSpeed - 2
count++
}
for (let i = 0; i < 4; i++) {
// 灰色刻度
speedGroup.addShape('rect', {
attrs: {
x: width * 4 / 5,
y: this.speedAxisY[i] + 2,
width: 15,
height: 2 * i + 1,
fill: '#fff',
opacity: 0.3
},
speed: speedNum[i] - 1,
name: 'speed-rect'
})
}
// 速度文本
this.speedText = speedGroup.addShape('text', {
attrs: {
x: width * 4 / 5 + 20,
y: this.speedAxisY[1] + 15,
text: '3.0X',
fill: '#ccc'
}
})
this.speedPoint = speedGroup.addShape('path', {
attrs: {
path: this.getPath(width * 4 / 5, this.speedAxisY[2]),
fill: '#ccc'
}
})
}
private getPath (x, y) {
return [
['M', x, y],
['L', x - 12, y - 6],
['L', x - 12, y + 6],
['Z']
]
}
private renderToggleTime() {
const { width, y } = this.controllerCfg
this.toggleGroup = this.group.addGroup({
name: 'toggle-group'
})
console.log(this.speedAxisY)
this.toggleGroup.addShape('rect', {
attrs: {
width: 14,
height: 14,
x: width - 45,
y: this.speedAxisY[1],
fill: '#fff',
stroke: '#ccc',
lineWidth: 2,
radius: 3
},
isChecked: false,
name: 'toggle-model'
})
this.checkedIcon = this.toggleGroup.addShape('path', {
attrs: {
path: [
['M', width - 45 + 3, this.speedAxisY[1] + 6],
['L', width - 45 + 7, this.speedAxisY[1] + 10],
['L', width - 45 + 12, this.speedAxisY[1] + 4],
],
stroke: 'green',
lineWidth: 3,
},
capture: false
})
this.checkedIcon.hide()
this.checkedText = this.toggleGroup.addShape('text', {
attrs: {
text: '单一时间',
x: width - 30,
y: this.speedAxisY[1] + 15,
fill: '#ccc'
}
})
}
private bindEvent() {
const { width } = this.controllerCfg
this.speedGroup.on('speed-rect:click', evt => {
this.speedPoint.attr('path', this.getPath(width * 4 / 5, evt.y))
this.currentSpeed = evt.target.get('speed')
this.speedText.attr('text', `${this.currentSpeed}.0X`)
this.group.emit('timebarConfigChanged', {
speed: this.currentSpeed,
type: this.currentType,
})
})
this.toggleGroup.on('toggle-model:click', evt => {
const isChecked = evt.target.get('isChecked')
if (!isChecked) {
this.checkedIcon.show()
this.checkedText.attr('text', '时间范围')
this.currentType = 'signle'
} else {
this.checkedIcon.hide()
this.checkedText.attr('text', '单一时间')
this.currentType = 'range'
}
evt.target.set('isChecked', !isChecked)
this.group.emit('timebarConfigChanged', {
type: this.currentType,
speed: this.currentSpeed
})
})
}
public destroy() {
this.group.off('playPauseBtn:click')
}
}

View File

@ -23,8 +23,8 @@ export interface HandlerCfg {
}
const DEFAULT_STYLE = {
fill: '#F7F7F7',
stroke: '#BFBFBF',
fill: '#1890ff',
stroke: '#1890ff',
radius: 2,
opacity: 1,
cursor: 'ew-resize',
@ -47,10 +47,11 @@ export default class Handler {
// 组件
private background: IShape;
private handleGroup: IGroup;
constructor(cfg: HandlerCfg) {
const { group, name, x = 0, y = 0, width = 10, height = 24, style = {} } = cfg;
const { group, name, x = 0, y = 0, width = 2, height = 24, style = {} } = cfg;
this.group = group
this.name = name
@ -95,8 +96,9 @@ export default class Handler {
const { width, height, style, name } = this;
const { fill, stroke, radius, opacity, cursor } = style;
this.handleGroup = this.group.addGroup()
// 按钮框框
this.background = this.group.addShape('rect', {
this.background = this.handleGroup.addShape('rect', {
attrs: {
x: 0,
y: 0,
@ -111,38 +113,31 @@ export default class Handler {
name: `${name}-rect`
});
// 两根竖线
const x1 = 1 / 3 * width;
const x2 = 2 / 3 * width;
const y1 = 1 / 4 * height;
const y2 = 3 / 4 * height;
this.group.addShape('line', {
this.handleGroup.addShape('circle', {
attrs: {
x1,
y1,
x2: x1,
y2,
x: width / 2,
y: 0,
r: 2 * width,
fill,
stroke,
cursor,
},
capture: false
// name
});
radius,
opacity,
cursor
}
})
this.group.addShape('line', {
this.handleGroup.addShape('circle', {
attrs: {
x1: x2,
y1,
x2,
y2,
x: width / 2,
y: height,
r: 2 * width,
fill,
stroke,
cursor,
},
capture: false
// name
});
radius,
opacity,
cursor
}
})
// 移动到对应的位置
this.updateXY();
@ -152,18 +147,26 @@ export default class Handler {
private bindEvents() {
const { name } = this;
this.group.on(`${name}-rect:mouseenter`, () => {
this.handleGroup.on(`${name}-rect:mouseenter`, () => {
const { highLightFill } = this.style;
this.background.attr('fill', highLightFill);
});
this.group.on(`${name}-rect:mouseleave`, () => {
this.handleGroup.on(`${name}-rect:mouseleave`, () => {
const { fill } = this.style;
this.background.attr('fill', fill);
});
}
public show() {
this.handleGroup.show()
}
public hide() {
this.handleGroup.hide()
}
private updateXY() {
this.group.setMatrix([1, 0, 0, 0, 1, 0, this.x, this.y, 1])
this.handleGroup.setMatrix([1, 0, 0, 0, 1, 0, this.x, this.y, 1])
}
}

View File

@ -7,7 +7,7 @@ import { IGroup, ICanvas } from '@antv/g-base';
import createDOM from '@antv/dom-util/lib/create-dom'
import { isString } from '@antv/util'
import Base, { IPluginBaseConfig } from '../base';
import Slider, { SliderOption, VALUE_CHANGE } from './slider'
import Slider, { SliderOption, VALUE_CHANGE, ControllerCfg } from './trendTimeBar'
import TimeLine, { TimeLineCfg, TIMELINE_CHANGE } from './timeLine'
import { IGraph } from '../../interface/graph';
import { GraphData } from '../../types';
@ -19,8 +19,24 @@ interface Callback {
target: IGroup;
}
interface TrendConfig extends TrendCfg{
interface TrendConfig {
readonly slider?: SliderOption;
// 数据
readonly data: {
date: string;
value: string;
}[];
// 位置大小
readonly x?: number;
readonly y?: number;
readonly width?: number;
readonly height?: number;
// 样式
readonly smooth?: boolean;
readonly isArea?: boolean;
readonly backgroundStyle?: object;
readonly lineStyle?: object;
readonly areaStyle?: object;
}
interface TimeBarConfig extends IPluginBaseConfig {
@ -31,11 +47,13 @@ interface TimeBarConfig extends IPluginBaseConfig {
readonly height?: number;
readonly padding?: number;
// 趋势图配置项,包括滑块、及前后背景的配置
// 趋势图配置项
readonly trend?: TrendConfig;
// 滑块、及前后背景的配置
readonly slider?: SliderOption;
// 时间线的配置项
readonly timeline?: TimeLineCfg;
// 控制按钮
readonly controllerCfg?: ControllerCfg;
// readonly opti
rangeChange?: (graph: IGraph, minValue: string, maxValue: string) => void;
@ -50,21 +68,22 @@ export default class TimeBar extends Base {
container: null,
className: 'g6-component-timebar',
padding: 10,
speed: 2,
loop: false,
trend: {
data: [],
group: null,
isArea: false,
smooth: true,
slider: {
minLimit: 0,
maxLimit: 1,
start: 0.1,
end: 0.9,
minText: 'min',
maxText: 'max',
}
smooth: true
},
controllerCfg: {
speed: 2,
loop: false,
},
slider: {
minLimit: 0,
maxLimit: 1,
start: 0.1,
end: 0.9,
minText: 'min',
maxText: 'max',
}
};
}
@ -125,14 +144,14 @@ export default class TimeBar extends Base {
this.set('timeBarGroup', timeBarGroup)
this.renderTrend()
this.renderTimeLine()
// this.renderTimeLine()
this.initEvent()
}
private renderTrend() {
const ratio = 0.6
const { width, height, x, y, padding, trend } = this._cfgs
const { slider, ...other } = trend
const { width, height, x, y, padding, trend, slider, controllerCfg } = this._cfgs
const { data, ...other } = trend
const realHeight = height - 2 * padding
const realWidth = width - 2 * padding
@ -140,56 +159,27 @@ export default class TimeBar extends Base {
graph: this.get('graph'),
canvas: this.get('canvas'),
group: this.get('timeBarGroup'),
x,
y,
x: x + padding,
y: y + padding,
width: realWidth,
height: realHeight * ratio,
padding,
trendCfg: {
...other,
data: other.data.map(d => d.value)
data: data.map(d => d.value)
},
...trend.slider,
...slider,
ticks: data.map(d => d.date),
handlerStyle: {
...slider.handlerStyle,
height: realHeight * ratio
}
},
controllerCfg
})
this.set('slider', sliderComponent)
}
private renderTimeLine() {
const ratio = 0.2
const { width, height, x, y, padding, timeline } = this._cfgs
const { speed, defaultCurrentTick, loop, axisCfg = {}, controllerCfg = {}, ticks } = timeline
const realHeight = height - 2 * padding
const realWidth = width - 2 * padding
const timelineComponent = new TimeLine({
graph: this.get('graph'),
canvas: this.get('canvas'),
group: this.get('timeBarGroup'),
x,
y: realHeight * 0.6,
width: realWidth,
height: realHeight * ratio,
speed,
defaultCurrentTick,
loop,
ticks,
axisCfg,
controllerCfg: {
x,
y: realHeight * (1 - ratio),
width: realWidth,
height: realHeight * ratio,
...controllerCfg
}
})
this.set('timeline', timelineComponent)
}
private filterData(evt) {
const { value } = evt;
const { data: trendData } = this._cfgs.trend
@ -275,20 +265,20 @@ export default class TimeBar extends Base {
}
private initEvent() {
const { start, end } = this._cfgs.trend.slider;
const { start, end } = this._cfgs.slider;
const graph: IGraph = this.get('graph');
graph.on('afterrender', () => {
this.filterData({ value: [start, end] });
});
graph.on(VALUE_CHANGE, (evt: Callback) => {
this.filterData(evt);
// this.filterData(evt);
});
// 时间轴的值发生改变的事件
graph.on(TIMELINE_CHANGE, (value: string) => {
this.renderCurrentData(value)
})
// graph.on(TIMELINE_CHANGE, (value: string) => {
// this.renderCurrentData(value)
// })
}
public destroy() {

View File

@ -62,7 +62,7 @@ export default class Button {
this.startMarker = group.addShape('path', {
attrs: {
path: this.getStartMarkerPath(),
fill: '#ffffff',
fill: '#ccc',
},
name: 'playPauseBtn'
});
@ -76,7 +76,7 @@ export default class Button {
y: this.config.y - height / 2,
width: width * 2.5,
height,
...style
fill: '#fff'
},
name: 'playPauseBtn'
})
@ -86,7 +86,7 @@ export default class Button {
y: this.config.y - height / 2,
width: width,
height: height,
fill: '#ffffff',
fill: '#ccc',
},
name: 'playPauseBtn'
});
@ -97,7 +97,7 @@ export default class Button {
y: this.config.y - height / 2,
width,
height,
fill: '#ffffff',
fill: '#ccc',
},
name: 'playPauseBtn'
});

View File

@ -1,12 +1,8 @@
/**
* @file G Slider
* @author hustcc
*/
import { Event, IGroup, ICanvas, IShape } from '@antv/g-base';
import { get, size, assign, each } from '@antv/util';
import Trend, { TrendCfg } from './trend';
import Handler from './handler';
import ControllerBtn from './controllerBtn'
import { IGraph } from '../../interface/graph';
import { ShapeStyle } from '../../types';
@ -25,7 +21,7 @@ export const FOREGROUND_STYLE = {
cursor: 'move',
};
export const DEFAULT_HANDLER_WIDTH = 10;
export const DEFAULT_HANDLER_WIDTH = 2;
export const HANDLER_STYLE = {
width: DEFAULT_HANDLER_WIDTH,
@ -38,8 +34,30 @@ export const TEXT_STYLE = {
opacity: 0.45,
};
export const TIMELINE_START = 'timelinestart';
export const TIMELINE_CHANGE = 'timelinechange';
export const TIMELINE_END = 'timelineend';
export const VALUE_CHANGE = 'valueChange';
export type ControllerCfg = Partial<{
readonly x: number;
readonly y: number;
readonly width: number;
readonly height: number;
readonly padding: number;
/** 播放速度1 个 tick 花费时间 */
readonly speed?: number;
/** 是否循环播放 */
readonly loop?: boolean;
readonly fill: string;
readonly stroke: string;
readonly preBtnStyle: ShapeStyle;
readonly nextBtnStyle: ShapeStyle;
readonly playBtnStyle: ShapeStyle;
}>
export type SliderOption = Partial<{
readonly backgroundStyle?: ShapeStyle;
readonly foregroundStyle?: ShapeStyle;
@ -61,7 +79,7 @@ export type SliderOption = Partial<{
readonly maxText: string;
}>;
interface SliderCfg extends SliderOption {
interface TrendTimeBarConfig extends SliderOption {
readonly graph: IGraph;
readonly canvas: ICanvas;
readonly group: IGroup;
@ -70,12 +88,16 @@ interface SliderCfg extends SliderOption {
readonly y: number;
readonly width: number;
readonly height: number;
readonly padding: number;
// style
readonly trendCfg?: TrendCfg;
readonly ticks?: string[];
readonly controllerCfg: ControllerCfg;
}
export default class Slider {
export default class TrendTimeBar{
private group: IGroup;
private graph: IGraph;
private canvas: ICanvas;
@ -84,8 +106,11 @@ export default class Slider {
public y: number;
public width: number;
public height: number;
private padding: number;
private trendCfg: TrendCfg;
private controllerCfg: ControllerCfg;
// 样式配置
private backgroundStyle: any;
private foregroundStyle: any;
@ -102,6 +127,8 @@ export default class Slider {
/* 右侧文本 */
private maxTextShape: IShape;
private textList: IShape[];
// 交互相关的数据信息
private start: number;
private end: number;
@ -110,17 +137,32 @@ export default class Slider {
private currentHandler: Handler | IShape;
private prevX: number = 0;
private prevY: number = 0;
private sliderGroup: IGroup;
constructor(cfg: SliderCfg) {
/** 刻度位置预处理 */
private tickPosList: number[];
private ticks: string[];
/** 是否处于播放状态 */
private isPlay: boolean;
/** 动画 id */
private playHandler: number;
private controllerBtnGroup: ControllerBtn;
constructor(cfg: TrendTimeBarConfig) {
const {
x = 0,
y = 0,
width = 100,
height = 16,
padding = 10,
trendCfg,
controllerCfg = {
speed: 1
},
backgroundStyle = {},
foregroundStyle = {},
handlerStyle = {},
@ -132,7 +174,8 @@ export default class Slider {
maxText = '',
group,
graph,
canvas
canvas,
ticks
} = cfg;
this.graph = graph
@ -143,8 +186,10 @@ export default class Slider {
this.y = y;
this.width = width;
this.height = height;
this.padding = padding
this.ticks = ticks
this.trendCfg = trendCfg;
this.controllerCfg = controllerCfg;
// style
this.backgroundStyle = { ...BACKGROUND_STYLE, ...backgroundStyle };
this.foregroundStyle = { ...FOREGROUND_STYLE, ...foregroundStyle };
@ -165,7 +210,7 @@ export default class Slider {
*
* @param cfg
*/
public update(cfg: Partial<SliderCfg>) {
public update(cfg: Partial<TrendTimeBarConfig>) {
const { x, y, width, height, minText, maxText, start, end } = cfg;
// start、end 只能是 0~1 范围
@ -203,8 +248,8 @@ export default class Slider {
// 趋势图数据
if (size(get(this.trendCfg, 'data'))) {
new Trend({
x: 0,
y: 0,
x: this.x,
y: this.y,
width,
height,
...this.trendCfg,
@ -213,10 +258,11 @@ export default class Slider {
}
const sliderGroup = this.group.addGroup({
name: 'slider-group'
name: 'slider-group',
// x: this.x,
// y: this.y,
})
this.sliderGroup = sliderGroup
// 1. 背景
sliderGroup.addShape('rect', {
attrs: {
@ -228,11 +274,12 @@ export default class Slider {
},
});
const textGroup = this.group.addGroup()
// 2. 左右文字
this.minTextShape = sliderGroup.addShape('text', {
this.minTextShape = textGroup.addShape('text', {
attrs: {
// x: 0,
y: height / 2,
x: 0,
y: height / 2 + this.y,
textAlign: 'right',
text: this.minText,
silent: false,
@ -241,10 +288,10 @@ export default class Slider {
capture: false
});
this.maxTextShape = sliderGroup.addShape('text', {
this.maxTextShape = textGroup.addShape('text', {
attrs: {
// x: 0,
y: height / 2,
y: height / 2 + this.y,
textAlign: 'left',
text: this.maxText,
silent: false,
@ -255,29 +302,31 @@ export default class Slider {
});
// 3. 前景 选中背景框
this.foregroundShape = sliderGroup.addShape('rect', {
this.foregroundShape = this.group.addGroup().addShape('rect', {
attrs: {
// x: 0,
y: 0,
// width: 0,
x: 0,
y: this.y,
// width,
height,
...this.foregroundStyle,
},
});
// 滑块相关的大小信息
const handlerWidth = get(this.handlerStyle, 'width', 10);
const handlerWidth = get(this.handlerStyle, 'width', 2);
const handlerHeight = get(this.handlerStyle, 'height', 24);
const minHandleGroup = this.group.addGroup({
name: 'minHandlerShape'
name: 'minHandlerShape',
// x: this.x,
// y: this.y,
})
// 4. 左右滑块
this.minHandlerShape = new Handler({
name: 'minHandlerShape',
group: minHandleGroup,
x: 0,
y: 0,//(height - handlerHeight) / 2,
x: this.x,
y: this.y,//(height - handlerHeight) / 2,
width: handlerWidth,
height: handlerHeight,
cursor: 'ew-resize',
@ -285,24 +334,73 @@ export default class Slider {
});
const maxHandleGroup = this.group.addGroup({
name: 'maxHandlerShape'
name: 'maxHandlerShape',
// x: this.x,
// y: this.y,
})
this.maxHandlerShape = new Handler({
name: 'maxHandlerShape',
group: maxHandleGroup,
x: 0,
y: 0,//(height - handlerHeight) / 2,
x: this.x,
y: this.y,
width: handlerWidth,
height: handlerHeight,
cursor: 'ew-resize',
...this.handlerStyle,
});
// 缩略图下面的时间刻度
const tickData = this.ticks
const interval = width / (tickData.length - 1);
this.tickPosList = [];
if (this.textList && this.textList.length) {
this.textList.forEach((text) => {
text.destroy();
});
}
let lastX = -Infinity;
this.textList = tickData.map((tick, index) => {
this.tickPosList.push(this.x + index * interval);
const text = this.group.addShape('text', {
attrs: {
x: this.x + index * interval,
y: this.y + height + 5,
text: tick,
textAlign: 'center',
textBaseline: 'top',
fill: '#607889',
opacity: 0.35,
},
});
const bbox = text.getBBox();
// 抽样,标签与标签间距不小于 10
if (bbox.minX > lastX) {
text.show();
lastX = bbox.minX + bbox.width + 10;
} else {
text.hide();
}
return text;
});
// 渲染播放、快进和后退的控制按钮
this.controllerBtnGroup = new ControllerBtn({
group: this.group,
x: this.x,
y: this.y + height + 25,
width,
height: 40
})
// 根据 start end 更新 ui 的位置信息
this.updateUI();
// 移动到对应的位置
this.sliderGroup.move(this.x, this.y);
sliderGroup.move(this.x, this.y);
// 绑定事件鼠标事件
this.bindEvents();
@ -334,6 +432,45 @@ export default class Slider {
// 3. 前景选中区域
this.foregroundShape.on('mousedown', this.onMouseDown(this.foregroundShape));
this.foregroundShape.on('touchstart', this.onMouseDown(this.foregroundShape));
// 播放区按钮控制
/** 播放/暂停事件 */
this.group.on('playPauseBtn:click', () => {
this.isPlay = !this.isPlay;
this.currentHandler = this.maxHandlerShape
this.changePlayStatus();
})
// 处理前进一步的事件
this.group.on('nextStepBtn:click', () => {
this.currentHandler = this.maxHandlerShape
this.updateStartEnd(0.01);
this.updateUI()
})
// 处理后退一步的事件
this.group.on('preStepBtn:click', () => {
this.currentHandler = this.maxHandlerShape
this.updateStartEnd(-0.01);
this.updateUI()
})
this.group.on('timebarConfigChanged', ({ type, speed }) => {
console.log('timebarConfigChanged', type, speed)
if(type === 'signle') {
this.minHandlerShape.hide()
this.foregroundShape.hide()
this.minTextShape.hide()
} else if (type === 'range') {
this.minHandlerShape.show()
this.foregroundShape.show()
this.minTextShape.show()
}
})
/** 播放轴上圆点滑动事件 */
// this.timeSelect.on('mousedown', this.onTimeSelectMouseDown);
}
private onMouseDown = (handler: Handler | IShape) => (event: Event) => {
@ -347,7 +484,6 @@ export default class Slider {
// 兼容移动端获取数据
this.prevX = get(event, 'touches.0.pageX', event.x);
this.prevY = get(event, 'touches.0.pageY', event.y);
// 3. 开始滑动的时候,绑定 move 和 up 事件
const containerDOM = this.canvas.get('container');
@ -368,8 +504,6 @@ export default class Slider {
e.preventDefault();
const x = get(e, 'touches.0.pageX', e.pageX);
const y = get(e, 'touches.0.pageY', e.pageY);
console.log('slider move', e, x, y)
// 横向的 slider 只处理 x
const offsetX = x - this.prevX;
@ -382,9 +516,6 @@ export default class Slider {
this.updateUI();
this.prevX = x;
this.prevY = y;
// this.get('canvas').draw();
// 因为存储的 start、end 可能不一定是按大小存储的,所以排序一下,对外是 end >= start
this.graph.emit(VALUE_CHANGE, {value: [this.start, this.end].sort()});
@ -410,6 +541,17 @@ export default class Slider {
}
};
/** 输入当前圆点位置,输出离哪个 tick 的位置最近 */
private adjustTickIndex(timeSelectX: number) {
for (let i = 0; i < this.tickPosList.length - 1; i++) {
if (this.tickPosList[i] <= timeSelectX && timeSelectX <= this.tickPosList[i + 1]) {
return Math.abs(this.tickPosList[i] - timeSelectX) < Math.abs(timeSelectX - this.tickPosList[i + 1])
? i
: i + 1;
}
}
}
/**
* offsetRange
* @param offsetRange
@ -445,13 +587,21 @@ export default class Slider {
switch (this.currentHandler) {
case this.minHandlerShape:
this.start += offsetRange;
const minTick = this.adjustTickIndex(this.start * this.width)
this.minText = this.ticks[minTick]
break;
case this.maxHandlerShape:
this.end += offsetRange;
const maxTick = this.adjustTickIndex(this.end * this.width)
this.maxText = this.ticks[maxTick]
break;
case this.foregroundShape:
this.start += offsetRange;
this.end += offsetRange;
const minRangeTick = this.adjustTickIndex(this.start * this.width)
const maxRangeTick = this.adjustTickIndex(this.end * this.width)
this.minText = this.ticks[minRangeTick]
this.maxText = this.ticks[maxRangeTick]
break;
}
}
@ -461,9 +611,16 @@ export default class Slider {
* @private
*/
private updateUI() {
if (this.start < 0) {
this.start = 0
}
if (this.end > 1) {
this.end = 1
}
const min = this.start * this.width;
const max = this.end * this.width;
// 1. foreground
this.foregroundShape.attr('x', min);
this.foregroundShape.attr('width', max - min);
@ -472,8 +629,9 @@ export default class Slider {
const handlerWidth = get(this.handlerStyle, 'width', DEFAULT_HANDLER_WIDTH);
// 设置文本
this.minTextShape.attr('text', this.minText);
this.maxTextShape.attr('text', this.maxText);
this.setText(this.minText, this.maxText)
// this.minTextShape.attr('text', this.minText);
// this.maxTextShape.attr('text', this.maxText);
const [minAttrs, maxAttrs] = this.dodgeText([min, max]);
// 2. 左侧滑块和文字位置
@ -524,4 +682,45 @@ export default class Slider {
return !sorted ? [minAttrs, maxAttrs] : [maxAttrs, minAttrs];
}
private startPlay() {
return window.requestAnimationFrame(() => {
const { controllerCfg, ticks, width } = this
const { speed } = controllerCfg
const tickInterval = width / ticks.length;
const offsetX = tickInterval / ((speed * 1000) / 60);
const offsetXRange = this.adjustOffsetRange(offsetX / this.width);
this.updateStartEnd(offsetXRange)
this.updateUI()
// this.setTimeSelectX(offsetX);
if (this.isPlay) {
this.playHandler = this.startPlay();
}
});
}
private changePlayStatus(isSync = true) {
this.controllerBtnGroup.playButton.update({
isPlay: this.isPlay,
});
if (this.isPlay) {
// 开始播放
this.playHandler = this.startPlay();
this.graph.emit(TIMELINE_START, null);
} else {
// 结束播放
if (this.playHandler) {
window.cancelAnimationFrame(this.playHandler);
if (isSync) {
// this.syncCurrnentTick();
this.graph.emit(TIMELINE_END, null);
}
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -7,6 +7,7 @@ import ChangeAttr from './attrs';
import DomClick from './component/dom-click';
import ForceLayout from './forceLayout';
import GGEditorNode from './ggeditorNode';
import MultiLayout from './forceLayout/multiLayout'
export default { title: 'Issues' };
@ -17,4 +18,5 @@ storiesOf('Issues', module)
.add('change attr', () => <ChangeAttr />)
.add('dom click', () => <DomClick />)
.add('forcelayout', () => <ForceLayout />)
.add('ggeditor node issue', () => <GGEditorNode />);
.add('ggeditor node issue', () => <GGEditorNode />)
.add('multi layout', () => <MultiLayout />)

View File

@ -198,97 +198,41 @@ describe.only('timeline play with timebar', () => {
});
}
const TrendData = [
21,
13,
19,
25,
18,
4,
21,
19,
22,
26,
3,
13,
5,
23,
24,
9,
29,
2,
27,
26,
29,
7,
19,
29,
15,
3,
9,
11,
29,
30,
4,
24,
23,
16,
21,
7,
24,
19,
18,
16,
1,
15,
27,
13,
23,
16,
4,
1,
1,
20,
];
const timeLine = new TimeBar({
x: 0,
y: 0,
width: 500,
height: 200,
padding: 10,
trend: {
data: timeBarData,
isArea: true,
// areaStyle: {
// fill: 'pink'
// },
// lineStyle: {
// stroke: 'green',
// lineWidth: 3
lineStyle: {
stroke: 'green',
lineWidth: 3
}
},
slider: {
// backgroundStyle: {
// fill: 'blue',
// opacity: 0.2
// },
// slider: {
// backgroundStyle: {
// fill: 'blue',
// opacity: 0.2
// },
// foregroundStyle: {
// fill: 'yellow'
// },
// textStyle: {
// fill: 'red',
// fontSize: 16,
// },
// handlerStyle: {
// style: {
// fill: 'green',
// stroke: 'red'
// }
// foregroundStyle: {
// fill: 'yellow'
// },
textStyle: {
fill: 'red',
fontSize: 16,
},
// handlerStyle: {
// style: {
// fill: '#1890ff',
// stroke: '#1890ff'
// }
// }
},
timeline: {
ticks: TrendData,
}
// loop: true
})