打造自己的图表控件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()

效果

下载

 

posted @ 2017-10-20 11:50  长蘑菇星人  阅读(689)  评论(2编辑  收藏  举报