打造自己的图表控件2
上次写了一个简单的折线图示例,但是显示的点的个数最多等于像素点的个数,这肯定是不满足需求的。
今天就来实现将无限的数据点投影到有限的像素上。
首先创建一个 Viewport 类,用来描述数据的范围,及数据点到像素的转换。
class Viewport { constructor() { this.visible = [0, 0, 0, 0]//fromX, fromY, toX, toY } setVisible(fromX, fromY, toX, toY) { this.visible = [fromX, fromY, toX, toY] } transform(data, [left, top, width, height]) { let result = [] let length = data.length / 2 let visibleLeft = this.visible[0] let visibleBottom = this.visible[1] let visibleWidth = this.visible[2] - visibleLeft let visibleHeight = this.visible[3] - visibleBottom let screenLeft = left let screenTop = top let screenWidth = width let screenHeight = height for (let i = 1; i < length; i++) { let x = screenLeft + (data[2 * i] - visibleLeft) / visibleWidth * screenWidth let y = screenTop + screenHeight - (data[2 * i + 1] - visibleBottom) / visibleHeight * screenHeight result.push(x) result.push(y) } return result } }
setVisible 设置窗口上显示的数据的范围
transform 用来将数据转换成像素点
然后给 Chart 添加一个 viewport 属性
class Chart { constructor() { this.elements = [] this._viewport = new Viewport() } get viewport() { return this._viewport } add(element) { element.attach(this) this.elements.push(element) } remove(element) { element.detach(this) this.elements.remove(element) } }
给ChartElement也添加上viewport属性
class ChartElement { constructor() { this._viewport = null } get viewport() { return this._viewport || this.chart && this.chart.viewport || null } set viewport(v) { this._viewport = v } attach(chart) { this.chart = chart } detach(chart) { this.chart = null } }
这里,就可以使用viewport进行转换了。
由于viewport需要知道窗口的大小和位置,所以以前 render 时只传入width和height 就不够了,这里改造一下 CanvasDrawingElement 的 render 函数来适配 viewport,别忘了要把 SampleLineDrawing 的 render 也修改一下
class CanvasDrawingElement extends ChartElement {
constructor() {
super()
}
render(context, [left, top, width, height]) {
}
}
class SampleLineDrawing extends CanvasDrawingElement {
constructor() {
super() this.data = null //[ x1,y1,x2,y2 ]
}
render(context, [left, top, width, height]) {
super.render(context, width, height)
if (this.data != null) {
context.strokeStyle = "#FF0000"
context.beginPath()
context.moveTo(this.data[0], this.data[1])
let length = this.data.length / 2
for (let i = 1; i < length; i++) {
let x = this.data[2 * i]
let y = this.data[2 * i + 1]
context.lineTo(x, y)
}
context.stroke()
}
}
}
同样 CanvasDrawing 也进行修改
class CanvasDrawing { constructor(width, height) { var canvas = this.canvas = document.createElement("canvas") canvas.width = width canvas.height = height this.width = width this.height = height this.context = canvas.getContext("2d") } init(dom) { dom.appendChild(this.canvas); } renderChart(chart) { let context = this.context context.clearRect(0, 0, this.width, this.height) for (let element of chart.elements) { if (element instanceof CanvasDrawingElement) { context.save() element.render(context, [0, 0, this.width, this.height]) context.restore() } } } }
然后实现一个 LineDrawing 用来绘制折线图
class LineDrawing extends CanvasDrawingElement { constructor() { super() this.data = null //[ x1,y1,x2,y2 ] } render(context, screen) { super.render(context, screen) let data = this.data let viewport = this.viewport if (data != null) { let points = viewport.transform(data, screen) context.strokeStyle = "#FF0000" context.beginPath() context.moveTo(points[0], points[1]) let length = points.length / 2 for (let i = 1; i < length; i++) { let x = points[2 * i] let y = points[2 * i + 1] context.lineTo(x, y) } context.stroke() } } }
这样就可以指定显示的范围了。
最后测试一下
var width = 800 var height = 600 var dataCount = width * 2 var chart = new Chart() chart.viewport.setVisible(0, -2, dataCount, 2) var lineDrawing = new LineDrawing() chart.add(lineDrawing) var chartDrawing = new CanvasDrawing(width, height) chartDrawing.init(document.body) var step = 0 function run() { requestAnimationFrame(run) step += 1 / 60 lineDrawing.data = [] for (var i = 0; i < dataCount; i++) { lineDrawing.data.push(i) lineDrawing.data.push(Math.sin(step + i * (360 * 4 / width) * Math.PI / 180)) } chartDrawing.renderChart(chart) } run()
到此结束,下期实现简单的X,Y坐标轴