打造自己的图表控件3
上期实现了数据投影的功能,现在就可以来实现坐标轴了。
以前只是整个画板范围内进行绘制,现在如果要进行坐标轴绘制,就要给画板分不同区域。
const ChartArea = { plot: 'plot', xAxis: 'xAxis', yAxis: 'yAxis', }
然后给 ChartElement 添加一个 area 属性 默认绘制到 plot 上
class ChartElement { ... get area() { return ChartArea.plot } ... }
然后实现一个 Axis ,area 默认到 xAxis,ticks 是刻度值的列表,labels 是刻度值字符串形式的列表。
range 获得现在视场内值的范围。
class Axis extends ChartElement { constructor() { super() this._ticks = [] this._labels = [] this.ticksCount = 20 this.ticksLength = 10 } get area() { return ChartArea.xAxis } get ticks() { return this._ticks } get labels() { return this._labels } get range() { var viewport = this.viewport if (this.area == ChartArea.yAxis) { return [viewport.visible[1], viewport.visible[3]] } else { return [viewport.visible[0], viewport.visible[2]] } }
calcTicks() { }
}
由于坐标轴有不同的表示方法,有的用时间表示,有的用离散的点表示,有的用连续的值表示。
这里只实现连续的值画刻线的方法。
创建一个FloatAxis 类,继承Axis,然后实现 calcTicks 方法
class FloatAxis extends Axis { constructor() { super() } calcTicks() { let range = this.range let delta = range[1] - range[0] let log = Math.round(Math.log10(delta)) let min, max if (log > 0) { let pow = Math.pow(10, log - 1) min = Math.round(range[0] / pow) * pow max = Math.round(range[1] / pow) * pow } else { min = Math.round(range[0] * Math.pow(10, -log)) / Math.pow(10, -log) max = Math.round(range[1] * Math.pow(10, -log)) / Math.pow(10, -log) } let calcStep = (max - min) / this.ticksCount let step if (log > 0) { let pow = Math.pow(10, log - 1) step = Math.round(calcStep / pow) * pow } else { step = Math.round(calcStep * Math.pow(10, -log)) / Math.pow(10, -log) } if (step == 0) step = calcStep let ticks = [] let labels = [] let end = max + step let x = min while (x < end) { ticks.push(x) labels.push(this.formatLabel(x, log - 2)) x += step } this._ticks = ticks this._labels = labels } formatLabel(value, log) { if (log < 0) { return value.toFixed(-log) } else { return ~~value + "" } } }
到这里 刻度 已经可以被计算出来了。
一共有两个坐标轴 X轴 和 Y 轴,于是 创建两个类,表示这两个轴
class FloatHorizontalAxis extends FloatAxis { constructor() { super() } get area() { return ChartArea.xAxis } } class FloatVerticalAxis extends FloatAxis { constructor() { super() } get area() { return ChartArea.yAxis } }
坐标转换在 Viewport 中实现,所以为Viewport 添加两个方法
class Viewport { ... transformX(x, [left, top, width, height]) { let visibleLeft = this.visible[0] let visibleWidth = this.visible[2] - visibleLeft let screenLeft = left let screenWidth = width return screenLeft + (x - visibleLeft) / visibleWidth * screenWidth } transformY(y, [left, top, width, height]) { let visibleBottom = this.visible[1] let visibleHeight = this.visible[3] - visibleBottom let screenTop = top let screenHeight = height return screenTop + screenHeight - (y - visibleBottom) / visibleHeight * screenHeight } }
这里基础已经构建完毕,开始实现画图的部分。
class VerticalAxisDrawing extends FloatVerticalAxis { constructor() { super() } render(context, [left, top, width, height]) { context.beginPath() context.moveTo(left + width, top) context.lineTo(left + width, height) context.stroke() this.calcTicks() let ticks = this.ticks let labels = this.labels let ticksLength = this.ticksLength context.save() context.font = '14px sans-serif' let x = left + width - ticksLength for (let i = 0, length = ticks.length; i < length; i++) { let y = this.viewport.transformY(ticks[i], [left, top, width, height]) context.beginPath() context.moveTo(x, y) context.lineTo(x + ticksLength, y) context.stroke() context.fillText(labels[i], x - ticksLength - labels[i].length * 5, y + 7) } context.restore() } }
class HorizontalAxisDrawing extends FloatHorizontalAxis { constructor() { super() } render(context, [left, top, width, height]) { context.beginPath() context.moveTo(left, top) context.lineTo(left + width, top) context.stroke() this.calcTicks() let ticks = this.ticks let labels = this.labels let ticksLength = this.ticksLength context.save() context.font = '14px sans-serif' let y = top for (let i = 0, length = ticks.length; i < length; i++) { let x = this.viewport.transformX(ticks[i], [left, top, width, height]) context.beginPath() context.moveTo(x, y) context.lineTo(x, y + ticksLength) context.stroke() context.fillText(labels[i], x - labels[i].length * 4, y + ticksLength + 14) } context.restore() } }
距离成功只有一步了,现在开始改造 CanvasDrawing
现在整个图像被分成3个部分,plot,xAxis,yAxis,所以给 CanvasDrawing 添加一个screens属性,表示不同的区域
class CanvasDrawing { constructor(width, height) { ... this.screens = { [ChartArea.plot]: [50, 0, width - 50, height - 50], [ChartArea.xAxis]: [50, height - 50, width - 50, 50], [ChartArea.yAxis]: [0, 0, 50, height - 50], } } ... }
再添加获取要绘制的区域和尺寸的方法
class CanvasDrawing { ... getScreen(area) { return this.screens[area] } * getArea() { yield ChartArea.xAxis yield ChartArea.yAxis yield ChartArea.plot } ... }
然后实现一个过滤的方法获取在某区域内要绘制的元素
class CanvasDrawing { ... * getElements(chart, area) { var elements = chart.elements || [] for (let element of elements.filter(e => e.area == area && this.isDrawElement(e))) { yield element } } isDrawElement(element) { return [CanvasDrawingElement, HorizontalAxisDrawing, VerticalAxisDrawing] .some(type => element instanceof type) } }
为了能提高一点点性能,创建一个背景画布,先画到背景画布上,然后在画到要显示的画布上。
class CanvasDrawing { constructor(width, height) { var canvas = this.canvas = document.createElement("canvas") var view = this.view = document.createElement("canvas") canvas.width = width canvas.height = height view.width = width view.height = height this.width = width this.height = height this.context = canvas.getContext("2d") this.viewContext = view.getContext("2d") ... } init(dom) { dom.appendChild(this.view) } }
最后 实现 renderChart 方法
class CanvasDrawing { ... renderChart(chart) { let context = this.context context.save() context.fillStyle = "#ffffff" context.fillRect(0, 0, this.width, this.height) context.restore() for (let area of this.getArea()) { let screen = this.getScreen(area) for (let element of this.getElements(chart, area)) { context.save() element.render(context, screen) context.restore() } this.viewContext.drawImage(this.canvas, ...screen, ...screen) } } ... }
调用
var width = 800 var height = 600 var dataCount = 1000 var chart = new Chart() chart.viewport.setVisible(0, -2, dataCount, 2) chart.add(new VerticalAxisDrawing()) chart.add(new HorizontalAxisDrawing()) var chartDrawing = new CanvasDrawing(width, height) chartDrawing.init(document.body) var lines = []; for (let index = 0; index < 50; index++) { var lineDrawing = new LineDrawing() chart.add(lineDrawing) lines.push(lineDrawing); } 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) { console.log( ~~(1000 / (now - begintime))) } begintime = now step += 1 chart.viewport.setVisible(step, -1 * lines.length, dataCount + step, 1 * lines.length) 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 + step) lineDrawing.data.push((j+1) * Math.sin((step + i) * (360 * 4 / width) * Math.PI / 180)) } } chartDrawing.renderChart(chart) } run()
效果