From 1bb68f16cd61ad20721c59610cc55338aa6e430b Mon Sep 17 00:00:00 2001 From: baizn <576375879@qq.com> Date: Wed, 15 Jul 2020 14:47:48 +0800 Subject: [PATCH] feat: slider timebar component --- package.json | 4 +- src/index.ts | 5 +- src/plugins/index.ts | 4 +- src/plugins/timeBar/index.ts | 240 +++++++++++++++++++++++++++++ tests/unit/plugins/timebar-spec.ts | 91 +++++++++++ 5 files changed, 341 insertions(+), 3 deletions(-) create mode 100644 src/plugins/timeBar/index.ts create mode 100644 tests/unit/plugins/timebar-spec.ts diff --git a/package.json b/package.json index d5cb5e5670..3140a58ea7 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,8 @@ "**/*.{js,ts,tsx}": "npm run lint-staged:js" }, "dependencies": { + "@antv/color-util": "^2.0.5", + "@antv/component": "^0.6.1", "@antv/dom-util": "^2.0.1", "@antv/event-emitter": "~0.1.0", "@antv/g-base": "^0.4.1", @@ -125,4 +127,4 @@ "webpack-cli": "^3.3.10", "worker-loader": "^2.0.0" } -} \ No newline at end of file +} diff --git a/src/index.ts b/src/index.ts index 356c706dfd..75fdef24b9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,6 +20,7 @@ const Bundling = Plugins.Bundling; const Menu = Plugins.Menu; const ToolBar = Plugins.ToolBar const Tooltip = Plugins.Tooltip +const TimeBar = Plugins.TimeBar export { registerNode, @@ -38,7 +39,8 @@ export { registerBehavior, Algorithm, ToolBar, - Tooltip + Tooltip, + TimeBar }; export default { @@ -59,6 +61,7 @@ export default { Menu: Plugins.Menu, ToolBar: Plugins.ToolBar, Tooltip: Plugins.Tooltip, + TimeBar, Algorithm, Arrow, Marker diff --git a/src/plugins/index.ts b/src/plugins/index.ts index 94b4a89da4..c6c9d2e542 100644 --- a/src/plugins/index.ts +++ b/src/plugins/index.ts @@ -4,6 +4,7 @@ import Minimap from './minimap'; import Bundling from './bundling'; import ToolBar from './toolBar' import Tooltip from './tooltip' +import TimeBar from './timeBar' export default { Menu, @@ -11,5 +12,6 @@ export default { Minimap, Bundling, ToolBar, - Tooltip + Tooltip, + TimeBar }; diff --git a/src/plugins/timeBar/index.ts b/src/plugins/timeBar/index.ts new file mode 100644 index 0000000000..b12831bdcf --- /dev/null +++ b/src/plugins/timeBar/index.ts @@ -0,0 +1,240 @@ +import modifyCSS from '@antv/dom-util/lib/modify-css'; +import createDOM from '@antv/dom-util/lib/create-dom'; +import isString from '@antv/util/lib/is-string' +import { IGroup } from '@antv/g-base' +import { Canvas } from '@antv/g-canvas' +import { Slider } from '@antv/component' +import { ShapeStyle, GraphData } from '../../types'; +import Base, { IPluginBaseConfig } from '../base'; +import { IGraph } from '../../interface/graph'; + +interface Data { + date: string; + value: number; +} + +interface Callback { + originValue: number[]; + value: number[]; + target: IGroup; +} + +interface TrendConfig { + readonly data: Data[]; + // 样式 + readonly smooth?: boolean; + readonly isArea?: boolean; + readonly backgroundStyle?: ShapeStyle; + readonly lineStyle?: ShapeStyle; + readonly areaStyle?: ShapeStyle; +}; + +type TimeBarOption = Partial<{ + // position size + readonly x: number; + readonly y: number; + readonly width: number; + readonly height: number; + + readonly backgroundStyle: ShapeStyle; + readonly foregroundStyle: ShapeStyle; + readonly handlerStyle: ShapeStyle; + readonly textStyle: ShapeStyle; + // 允许滑动位置 + readonly minLimit: number; + readonly maxLimit: number; + // 初始位置 + readonly start: number; + readonly end: number; + // 滑块文本 + readonly minText: string; + readonly maxText: string; + + readonly trend: TrendConfig; +}>; + +interface TimeBarConfig extends IPluginBaseConfig { + width?: number; + height?: number; + timebar: TimeBarOption; + rangeChange?: (graph: IGraph, min: number, max: number) => void; +} + +export default class TimeBar extends Base { + private cacheGraphData: GraphData + + constructor(cfg?: TimeBarConfig) { + super(cfg); + } + + public getDefaultCfgs(): TimeBarConfig { + return { + width: 400, + height: 50, + rangeChange: null, + timebar: { + x: 10, + y: 10, + width: 400, + height: 26, + minLimit: 0.05, + maxLimit: 0.95, + start: 0.1, + end: 0.9, + } + }; + } + + public init() { + const timeBarConfig: TimeBarOption = this.get('timebar') + const { trend = {} as TrendConfig } = timeBarConfig + const { data = [] } = trend + + if (!data || data.length === 0) { + console.warn('TimeBar 中没有传入数据') + return + } + + const container = this.get('container') + + let timebar + if (!container) { + timebar = createDOM(`
`) + modifyCSS(timebar, { position: 'absolute' }); + document.body.appendChild(timebar) + } else if (isString(container)) { + timebar = createDOM(``) + modifyCSS(timebar, { position: 'absolute' }); + document.body.appendChild(timebar) + } else { + timebar = container + } + + this.set('timeBarContainer', timebar) + + this.initTimeBar(timebar) + } + + private initTimeBar(container: HTMLDivElement) { + const width = this.get('width') + const height = this.get('height') + const canvas = new Canvas({ + container, + width, + height, + }); + + const group = canvas.addGroup({ + id: 'timebar-plugin', + }) + + const timeBarConfig: TimeBarOption = this.get('timebar') + const { trend = {} as TrendConfig , ...option } = timeBarConfig + + const config = { + container: group, + minText: option.start, + maxText: option.end, + ...option + } + + // 是否显示 TimeBar 根据是否传入了数据来确定 + const { data = [], ...trendOption } = trend + + const trendData = data.map(d => d.value) + + config['trendCfg'] = { + ...trendOption, + data: trendData + } + + config.minText = data[0].date + config.maxText = data[data.length - 1].date + + this.set('trendData', data) + + console.log('配置项', config) + + const slider = new Slider(config) + + slider.init(); + slider.render() + + this.set('slider', slider) + + this.bindEvent() + } + + /** + * 当滑动时,最小值和最大值会变化,变化以后触发相应事件 + */ + private bindEvent() { + const graph: IGraph = this.get('graph') + const slider = this.get('slider') + const rangeChange = this.get('rangeChange') + const trendData: Data[] = this.get('trendData') + slider.on('valuechanged', (evt: Callback) => { + const { value } = evt + + const min = Math.round(trendData.length * value[0]) + let max = Math.round(trendData.length * value[1]) + max = max > trendData.length ? trendData.length : max + const minText = trendData[min].date + const maxText = trendData[max].date + + slider.set('minText', minText) + slider.set('maxText', maxText) + + if (rangeChange) { + rangeChange(graph, minText, maxText) + } else { + // 自动过滤数据,并渲染 graph + const graphData = graph.save() as GraphData + + if (!this.cacheGraphData) { + this.cacheGraphData = graphData + } + + // 过滤不在 min 和 max 范围内的节点 + const filterData = this.cacheGraphData.nodes.filter((d: any) => d.date >= minText && d.date <= maxText) + + const nodeIds = filterData.map(node => node.id) + + // 过滤 source 或 target 不在 min 和 max 范围内的边 + const fileterEdges = this.cacheGraphData.edges.filter(edge => nodeIds.includes(edge.source) && nodeIds.includes(edge.target)) + + graph.changeData({ + nodes: filterData, + edges: fileterEdges + }) + + } + }) + } + + public show() { + const slider = this.get('slider') + slider.show() + } + + public hide() { + const slider = this.get('slider') + slider.hide() + } + + public destroy() { + this.cacheGraphData = null + + const slider = this.get('slider') + + if (slider) { + slider.off('valuechanged') + slider.destroy() + } + + const timeBarContainer = this.get('timeBarContainer') + if (timeBarContainer) { + document.body.removeChild(timeBarContainer); + } + } +} diff --git a/tests/unit/plugins/timebar-spec.ts b/tests/unit/plugins/timebar-spec.ts new file mode 100644 index 0000000000..043a9b68a6 --- /dev/null +++ b/tests/unit/plugins/timebar-spec.ts @@ -0,0 +1,91 @@ +import G6 from '../../../src'; +const div = document.createElement('div'); +div.id = 'timebar-plugin'; +document.body.appendChild(div); + +const data = { + nodes: [ + { + id: 'node1', + label: 'node1', + x: 100, + y: 100 + }, + { + id: 'node2', + label: 'node2', + x: 150, + y: 300 + } + ], + edges: [ + { + source: 'node1', + target: 'node2' + } + ] +} + +for(let i = 0; i < 100; i++) { + const id = `node-${i}` + data.nodes.push({ + id, + label: `node${i}`, + date: `2020${i}`, + value: Math.round(Math.random() * 300) + }) + + const edgeId = i + 3 + data.edges.push({ + source: `node-${Math.round(Math.random() * 90)}`, + target: `node-${Math.round(Math.random() * 90)}` + }) +} + +describe('tooltip', () => { + it('tooltip with default', () => { + const timeBarData = [] + + for(let i = 0; i < 100; i++) { + timeBarData.push({ + date: `2020${i}`, + value: Math.round(Math.random() * 300) + }) + } + const timebar = new G6.TimeBar({ + timebar: { + trend: { + data: timeBarData, + isArea: false, + smooth: true, + } + } + }); + const tooltip = new G6.Tooltip() + + const graph = new G6.Graph({ + container: div, + width: 500, + height: 500, + plugins: [timebar, tooltip], + modes: { + default: ['drag-node', 'zoom-canvas', 'drag-canvas'] + }, + defaultEdge: { + style: { + lineAppendWidth: 20 + } + } + }); + + graph.data(data) + graph.render() + + const timebarPlugin = graph.get('plugins')[0] + console.log(timebarPlugin) + // expect(timebarPlugin.get('offset')).toBe(6) + // expect(timebarPlugin.get('tooltip').outerHTML).toBe(``) + + // graph.destroy() + }) +});