zoukankan      html  css  js  c++  java
  • 打造自己的图表控件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()

    效果

    下载

  • 相关阅读:
    mysql data type <----> java data type (数值)
    line number is important in Exceptions.
    dom4j 使用原生xpath 处理带命名空间的文档
    dom4j 通过 org.dom4j.XPath 设置命名空间来支持 带namespace 的 xpath
    dom4j 创建一个带命名空间的pom.xml 文件
    xml to xsd ; xsd to xml
    sax 动态切换 抓取感兴趣的内容(把element当做documnet 处理)
    d3.js <一>
    python学习进阶一
    Java *字格
  • 原文地址:https://www.cnblogs.com/cuifeipeng/p/7698760.html
Copyright © 2011-2022 走看看