打造自己的图表控件3

时间:2021-11-08 09:14:26

上期实现了数据投影的功能,现在就可以来实现坐标轴了。

以前只是整个画板范围内进行绘制,现在如果要进行坐标轴绘制,就要给画板分不同区域。

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()

效果

打造自己的图表控件3

下载