上期实现了数据投影的功能,现在就可以来实现坐标轴了。
以前只是整个画板范围内进行绘制,现在如果要进行坐标轴绘制,就要给画板分不同区域。
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 {calcTicks() { }}
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()
效果