上期实现了数据投影的功能,现在就可以来实现坐标轴了。
以前只是整个画板范围内进行绘制,现在如果要进行坐标轴绘制,就要给画板分不同区域。
- 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]]
- }
- }
由于坐标轴有不同的表示方法,有的用时间表示,有的用离散的点表示,有的用连续的值表示。
这里只实现连续的值画刻线的方法。
创建一个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()
效果
下载
来源: http://www.cnblogs.com/cuifeipeng/p/7698760.html