打造自己的图表控件4
上一期已经完成了一个雏形。可以显示一个完整的折线图。本期给图表加上鼠标互动的功能。
首先新增一个专门处理鼠标事件的类。
class MouseNavigation extends ChartElement { constructor() { super() } get area() { return ChartArea.plot } hit(mouseEventArgs) { return false } onMouseDown(mouseEventArgs) { } onMouseMove(mouseEventArgs) { } onMouseUp(mouseEventArgs) { } onMouseWheel(mouseEventArgs) { } }
hit 用来测试是否命中
MouseEventArgs 定义如下,封装了鼠标相对于图表的位置 x,y. 触发事件时的 位置 和 屏幕尺寸,还有原始的事件对象。
class MouseEventArgs { constructor(x, y, area, screen, e) { this.x = x this.y = y this.area = area this._isPrevent = false this.event = e this.screen = screen } get isPrevent() { return this._isPrevent } prevent() { this._isPrevent = true } }
然后改造一下 Chart ,让它可以触发这些事件
class Chart { ... mouseDown(mouseEventArgs) { this._raiseMouseEvent('onMouseDown', mouseEventArgs) } mouseMove(mouseEventArgs) { this._raiseMouseEvent('onMouseMove', mouseEventArgs) } mouseUp(mouseEventArgs) { this._raiseMouseEvent('onMouseUp', mouseEventArgs) } mouseWheel(mouseEventArgs) { this._raiseMouseEvent('onMouseWheel', mouseEventArgs) } * _hits(mouseEventArgs) { let list = [] for (let element of this.elements) { if (element instanceof MouseNavigation) { if (element.hit(mouseEventArgs)) { list.push(element) } } } list = list.reverse() for (let element of list) { yield element } } _raiseMouseEvent(key, mouseEventArgs) { for (let element of this._hits(mouseEventArgs)) { element[key](mouseEventArgs) if (mouseEventArgs.isPrevent) break } } }
最后,在 CanvasDrawing 中注册事件
由于在 init 中已经保存了 chart,所以 render 的时候就不用在传入 chart 了。
class CanvasDrawing { ... init(dom, chart) { this.chart = chart dom.appendChild(this.view) document.addEventListener("mousedown", e => { if (e.target == this.view) e.preventDefault() var mouseEventArgs = this.createMouseEventArgs(e) chart.mouseDown(mouseEventArgs) }) document.addEventListener("mouseup", e => { if (e.target == this.view) e.preventDefault() var mouseEventArgs = this.createMouseEventArgs(e) chart.mouseUp(mouseEventArgs) }) document.addEventListener("mousemove", e => { if (e.target == this.view) e.preventDefault() var mouseEventArgs = this.createMouseEventArgs(e) chart.mouseMove(mouseEventArgs) }) document.addEventListener("mousewheel", e => { if (e.target == this.view) e.preventDefault() var mouseEventArgs = this.createMouseEventArgs(e) chart.mouseWheel(mouseEventArgs) }) } createMouseEventArgs(e) { let offsetX = this.view.offsetLeft let offsetY = this.view.offsetTop let x = e.pageX - offsetX let y = e.pageY - offsetY let area = this.findArea(x, y) let screen = this.getScreen(area) return new MouseEventArgs(x, y, area, screen, e) } renderChart() { let chart = this.chart ... } findArea(x, y) { for (let area of this.getArea()) { let [x2, y2, width, height] = this.getScreen(area) if (x >= x2 && x < x2 + width && y >= y2 && y < y2 + height) return area } return null } ... }
最后实现一个 MouseNavigationDrawing 来支持拖拽和缩放。
class MouseNavigationDrawing extends MouseNavigation { constructor() { super() this.start = [] this.draging = false this.screen = [] } hit(mouseEventArgs) { if (this.draging) return true let e = mouseEventArgs if (e.area == this.area) return true return false } onMouseDown(mouseEventArgs) { let e = mouseEventArgs e.prevent() let ieLeftDown = this._isLeftDown(e) console.log(e) if (ieLeftDown) { this._startDrag(e) } } _startDrag(e) { this.start = [e.x, e.y] this.draging = true this.screen = e.screen } _stopDrag() { this.start = [0, 0] this.draging = false } _isLeftDown(e) { return e.event.button == 0 } onMouseMove(mouseEventArgs) { let e = mouseEventArgs if (this.draging && this._isLeftDown(e)) { e.prevent() let startX = this.start[0] let startY = this.start[1] let width = this.screen[2] let height = this.screen[3] var deltaX = e.x - startX var deltaY = e.y - startY let visibleLeft = this.viewport.visible[0] let visibleBottom = this.viewport.visible[1] let visibleWidth = this.viewport.visible[2] - visibleLeft let visibleHeight = this.viewport.visible[3] - visibleBottom var offsetX = deltaX / width * visibleWidth var offsetY = deltaY / height * visibleHeight this.start = [e.x, e.y] this.viewport.offset(-offsetX, offsetY) } } onMouseUp(mouseEventArgs) { let e = mouseEventArgs if (this.draging) { e.prevent() this._stopDrag() } } onMouseWheel(mouseEventArgs) { let e = mouseEventArgs e.prevent() let delta = 0.1 * e.event.wheelDeltaY / 120
this.viewport.zoom(-delta) } }
Viewport 添加 offset 和 zoom 来支持拖放 和 缩放
class Viewport {
...
offset(x, y) { let [fromX, fromY, toX, toY] = this.visible this.visible = [fromX + x, fromY + y, toX + x, toY + y] } center() { let [fromX, fromY, toX, toY] = this.visible return [fromX + (toX - fromX) * 0.5, fromY + (toY - fromY) * 0.5] } zoom(delta) { let [fromX, fromY, toX, toY] = this.visible let [x, y] = this.center() let ratio = 1 + delta let left = x - (x - fromX) * ratio let right = left + (toX - fromX) * ratio let bottom = y - (y - fromY) * ratio let top = bottom + (toY - fromY) * ratio this.visible = [left, bottom, right, top] }
}
最后调用一下
var width = 800 var height = 600 var dataCount = 1000 var chart = new Chart() chart.add(new VerticalAxisDrawing()) chart.add(new HorizontalAxisDrawing()) chart.add(new MouseNavigationDrawing()) var chartDrawing = new CanvasDrawing(width, height) chartDrawing.init(document.body, chart) var lines = [] for (let index = 0; index < 50; index++) { var lineDrawing = new LineDrawing() chart.add(lineDrawing) lines.push(lineDrawing); } chart.viewport.setVisible(0, -1 * lines.length, dataCount , 1 * lines.length) var e = s => document.querySelector(s) var fps = e('#fps') var step = 0 var begintime = +new Date() var count = 0 function run() { requestAnimationFrame(run) var now = +new Date() count = ((count + 1) % 16) if (count == 0) { fps.innerHTML = ~~(1000 / (now - begintime)) } begintime = now step += 1 for (let j = 0; j < lines.length; j++) { let lineDrawing = lines[j] lineDrawing.data = [] for (let i = 0; i < dataCount; i++) { lineDrawing.data.push(i) lineDrawing.data.push((j + 1) * Math.sin((step + i) * (360 * 4 / width) * Math.PI / 180)) } } chartDrawing.renderChart(chart) } run()
效果