• 数据可视化:canvas与ECharts入门使用


    本周准备一个分享,顺便记录一下入门时碰到的问题。

    官方文档:https://echarts.apache.org/zh/index.html

    0.关于Echarts

    ECharts.js是百度开源的一个数据可视化图表库。

    2018年,全球著名开源社区 Apache 基金会宣布“百度开源的 ECharts 项目全票通过进入 Apache 孵化器”。这是百度第一个进入国际顶级开源社区的项目。

    1.安装

    npm install echarts --save

    2.引入

    // 全部引入
    const echarts = require('echarts')
    
    // 按需引入
    // 引入 ECharts 主模块
    var echarts = require('echarts/lib/echarts');
    // 引入柱状图
    require('echarts/lib/chart/bar');
    // 引入提示框和标题组件
    require('echarts/lib/component/tooltip');
    require('echarts/lib/component/title');

    3.使用

    笔者搭配vue使用echarts,以下代码均包含vue.js相关代码。

    3.1 初始化

    echarts的初始化使用很简单,引入的echarts主模块里,可以调用init()初始话函数,给这个函数传入dom元素,再根据自己需要进行设置setOption即可。

    <div id='test'/>

    需要注意的是,你需要提前给定这个元素的宽高,特别是高度height,否则在初始化成功之后你会看不到它。

    3.2 数据获取与更新

    echarts支持异步加载和更新数据,同样只需要将新的数据传入setOption即可。

    /**
      option: any = {
        title: {
          text: '柱状图'
        },
        tooltip: {},
        xAxis: {
          data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子']
        },
        yAxis: {},
        series: [{
          name: '销量',
          type: 'bar',
          data: []
        }]
      }
    */
    
        initEchatrs() {
        this.ele = echarts.init(document.getElementById('test'))
        this.ele.setOption(this.option)
        this.ele.showLoading()
        window.onresize = this.ele.resize  // 随窗口尺寸变化调整自身尺寸
    
        this.getAsyncData()
      }
    
      getAsyncData() {
        setTimeout(() => {
          // 此处修改对象属性值,有些类似于能让watch监听到的变化才能生效
          // 直接对this.option.xx进行增删改是无法生效的,使用this.$set或者object.assign()
          this.$set(this.option, 'series', [{
            name: '销量',
            type: 'bar',
            data: [5, 20, 36, 10, 10, 20]
          }])
          this.ele.setOption(this.option)
          this.ele.hideLoading()
        }, 1500)

    3.3 事件和行为

    echarts中绑定事件通过on方法

    myChart.on('click', function (params) {
        // 控制台打印数据的名称
        console.log(params.name);
    });

    echarts里的事件类型包括鼠标事件和使用可以交互的组件(如缩放数据区域等)后触发的行为事件。

    它支持常规的鼠标事件类型,包括click、dbclick、mousedown、mousemove、mouseup、mouseover、mouseout等,将对应的事件名传入on方法即可,回调事件包含参数params,它包含一个点击图形的数据信息的对象

    {
        // 当前点击的图形元素所属的组件名称,
        // 其值如 'series'、'markLine'、'markPoint'、'timeLine' 等。
        componentType: string,
        // 系列类型。值可能为:'line'、'bar'、'pie' 等。当 componentType 为 'series' 时有意义。
        seriesType: string,
        // 系列在传入的 option.series 中的 index。当 componentType 为 'series' 时有意义。
        seriesIndex: number,
        // 系列名称。当 componentType 为 'series' 时有意义。
        seriesName: string,
        // 数据名,类目名
        name: string,
        // 数据在传入的 data 数组中的 index
        dataIndex: number,
        // 传入的原始数据项
        data: Object,
        // sankey、graph 等图表同时含有 nodeData 和 edgeData 两种 data,
        // dataType 的值会是 'node' 或者 'edge',表示当前点击在 node 还是 edge 上。
        // 其他大部分图表中只有一种 data,dataType 无意义。
        dataType: string,
        // 传入的数据值
        value: number|Array
        // 数据图形的颜色。当 componentType 为 'series' 时有意义。
        color: string
    }

    on事件还支持query,query可以只对指定的图形元素触发回调。

    chart.on(eventName, query, handler);

    query可以为字符串

    chart.on('click', 'series', function () {...});
    chart.on('click', 'series.line', function () {...});
    chart.on('click', 'dataZoom', function () {...});
    chart.on('click', 'xAxis.category', function () {...});

    也可以为对象object,它包含以下可选属性

    {
        <mainType>Index: number // 组件 index
        <mainType>Name: string // 组件 name
        <mainType>Id: string // 组件 id
        dataIndex: number // 数据项 index
        name: string // 数据项 name
        dataType: string // 数据项 type,如关系图中的 'node', 'edge'
        element: string // 自定义系列中的 el 的 name
    }

    chart.setOption({
        // ...
        series: [{
            name: 'uuu'
            // ...
        }]
    });
    chart.on('mouseover', {seriesName: 'uuu'}, function () {
        // series name 为 'uuu' 的系列中的图形元素被 'mouseover' 时,此方法被回调。
    });

    其他支持的组件交互行为都会触发对应的事件,在官方文档中均有说明

    3.4 绘制svg

    echarts最开始是使用canvas绘制图表的,目前echarts 4.0以上已经支持svg绘制。

    只需要在init中的参数中设置render参数即可。

    // init: Function
    (dom: HTMLDivElement|HTMLCanvasElement, theme?: Object|string, opts?: {
        devicePixelRatio?: number,
        renderer?: string,
        width?: number|string,
        height?: number|string
    }) => ECharts
    
    // dom: 实例的容器,一般是一个具有宽高的div元素
    // theme: 应用主题
    // opts: 附加参数,可选项有:
    //                         devicePixelRatio: 设备像素比,默认取浏览器的window.devicePixelRatio
    //                         renderer: 渲染器,可选'canvas'或者'svg'
    //                          实例宽度,px
    //                         height: 实例高度,px

    3.5 其他api

    echarts的api文档写的十分详尽,参考它的官方文档,使用你想使用的组件即可。

    4.canvas

    canvas与svg都是浏览器端绘制图形的手段,但是他们在根本上是不同的。仅在绘图方面,svg的优势在于不会失真,渲染性能略高(因此更适合移动端)、内存占用更低;canvas更适合绘制图形元素数量非常大的图表。

    echarts的根本是对canvas/svg的操作,它的底层使用了zRender来绘制。

    echarts的github:https://github.com/apache/incubator-echarts

    这里仅讨论canvas。

    <canvas>标签提供了一块空白的画布容器,它公开了一个或多个渲染上下文,需要通过脚本在上面绘制。使用原生的canvas绘制图形,需要对canvas的api使用熟练。

    canvas api:https://www.runoob.com/tags/ref-canvas.html

    具体代码可以参考后面,先记录一下实际中踩到的几个坑:

    4.1 绘制1px线

    参考:https://www.cnblogs.com/v-rockyli/p/3833845.html

    下面两张图分别是处理前和处理后的效果

              处理后

              处理前

    仔细观察可以发现下图坐标轴和对齐轴的线条,比起上图看起来要粗一些、颜色浅一些。实际上两张图内绘制的线条都是1px。为什么会有这种区别呢?

    这跟canvas的绘制逻辑有关,当我们试图绘制一个线段时,canvas会读取lineWidth,,然后尝试将在坐标处两边各绘制一半的lineWidth。

     

    比如我们想在坐标(0,3)处绘制一条横线,canvas会以3为中轴线,在两边各画0.5像素,深蓝色就是我们期望的效果(2.5-3.5,1个像素),但实际上,浅蓝色也会被绘制出来,因为canvas无法在整个像素宽内只绘制半个像素,所以坐标轴上下两个方向都都会被扩展至整个像素宽度内(2-4,两个像素),但是扩展的像素实际的值并不是原值相同,而是取其一半,所以最直接的视觉感受是:线条比预想的变宽了,但是颜色浅了很多。

    还是以宽为1的横线为例,我们如果将其绘制在纵坐标2.5处,即以半像素作为中轴线

     

    同样浏览器进行绘制时,在2.5上下各绘制0.5的像素宽度,但与上面的例子不同的是,图像边界正好落在整数像素边界内,合起来正好为1个像素,这个时候,就不需要向两边扩展,而是我们预期的的1个像素宽度。

    同理,我们分别使用两种方式绘制宽度为2的线段时,效果恰恰相反,在坐标3处绘制的时候,像素正好扩展至2-4,即2个像素,符合我们的预期;而在坐标2.5处绘制时,像素扩展至1.5-3.5,未到边界,需要补足,就变成了1-4,即3个像素。

    因此在实际应用中,如果想得到更好的体验,精确的像素值,如果线段的宽度是奇数像素,绘制时以n.5,即半数像素作为中轴线,如果线段的宽度为偶数像素,绘制时以n.0,即整数像素作为中轴线

    4.2 视口与画布

    canvas与svg一样 存在视口的概念,不指定canvas标签的宽高属性时, 默认为300*150。如果在内部样式中指定canvas画布的大小,而不指定视口大小,在实际中会影响到绘制效果。
    我们指定画布大小为300*300,下图是视口宽度300时,分别指定视口高度150与300的效果

            视口高度150

            视口高度300

    可以明显看出效果会按照视口与画布的尺寸进行等比缩放;当视口高度与画布高度1:1的时候,看起来效果是最好。因此指定画布大小的时候,最好也指定canvas视口宽高。

    <canvas id="canvas" :width="width" :height="height"></canvas>
    
     width = 300
     height = 300
    
    #canvas {
       300px;
      height: 300px;
    } 

    但是我们观察echarts的视口与画布大小,可以看出echarts默认视口:画布=2:1,这样做也是有道理的,视口是画布的2倍,可以在缩放到200%的情况下不失真。

     因此如果只是想简单实现一个表格,不需要考虑缩放的效果的话,直接指定视口:画布=1:1即可(移动端大多数可以如此);如果需要考虑缩放n倍不失真(比如pc端浏览器的缩放功能),就需要增加视口宽高。

    4.3 绘制path

    canvas的绘制方法中比较重要的一个手段就是绘制path,其中比较值得注意的是beginPath和closePath。

    其中个人认为beginPath要比closePath要重要。从api的解释来看,beginPath有“重置当前路径”的功能,closePath只是创建一条新的路径,帮忙把这个图形闭合了而已。

    实际操作中不注意begin和close会引起的问题:

          图1 期望效果

          图2 无closePath

          图3 无beginPath

    这里图1的绘制流程可以查看下面代码

      getAsyncData() {
        setTimeout(() => {
          this.yAxis = [5, 20, 36, 10, 10, 20]
          // y坐标轴
          // 计算刻度
          const num = Math.ceil((Math.max(...this.yAxis)) / this.minUnit)
          const yGap = this.yAxisLen / num
          const max = num * this.minUnit
    
          // 绘制刻度及对齐轴
          for(let i = 0; i < num; i++) {
            const text = this.minUnit * (i + 1) + ''
            // 文本
            this.context.fillText(text, this.x0 - text.length * this.fontsize / 1.5, this.height - (10 + yGap * (i + 1) + this.fontsize / 2))
            // 刻度
            this.context.beginPath()
            const yUnit = Math.floor(this.y0 - this.yAxisLen + yGap * i) + 0.5
            this.context.moveTo(this.x0, yUnit)
            this.context.lineTo(this.x0 - 3, yUnit)
            this.context.strokeStyle = '#000000'
            this.context.stroke()
            // 对齐轴
            this.context.beginPath()
            this.context.moveTo(this.x0, yUnit)
            this.context.lineTo(this.width, yUnit)
            this.context.strokeStyle = '#eeeeee'
            this.context.stroke()
          }
    
          // 绘制条状图
          const xGap = (this.width - this.x0) / this.xAxis.length
          this.context.fillStyle = this.barColor
          this.barList = []
          this.start = new Array(this.yAxis.length).fill(0)   // 初始化帧数计数数组
          this.yAxis.map((data, index) => {
            const x1 = this.x0 + xGap * (index + 0.5) - this.barWidth / 2 
            const h = data / this.minUnit * yGap 
            const y1 = this.y0 - h
            const barData = {x: x1, y: y1, w: this.barWidth, h}
            // 保存色块数据
            this.barList.splice(index, 0, Object.assign({}, barData, {
              data,
              index,
              name: this.xAxis[index],
              color: this.barColor
            }))
            // 通过path绘制矩形
            // this.drawRectByPath(x1, y1, this.barWidth, h)
            // 添加动画效果
            this.animate(barData, index)
          })
        }, 1500);
      }
    
      drawRectByPath(x: number, y: number, w: number, h: number) {
        // 通过路径绘制矩形,x,y为左上角,w为宽,h为高
        // 如果不重置路径(beginPath)那么会从上一次的beginPath开始执行
        this.context.beginPath()
        this.context.moveTo(x, y)
        this.context.lineTo(x + w, y)
        this.context.lineTo(x + w, y + h)
        this.context.lineTo(x, y + h)
        // 如果不closePath,该路径则无法闭合,只会影响描边,不影响填充
        this.context.closePath()
        this.context.strokeStyle = 'rgba(0, 0, 0, 0.1)'
        this.context.fillStyle = 'rgba(200, 0, 0, 0.1)'
        // 未闭合路径也会自动回到开始路径并填充,但是stroke不会
        this.context.stroke()
        this.context.fill()
      }

    简单来说是,从上到下绘制刻度轴,从左到右依次绘制条状,path的路径是从左上角开始顺时针连线,颜色是有透明度的。

    图1是正常效果,图2是绘制条状时未执行closePath,图3是绘制条状时未执行beginPath。

    图2中不执行closePath,可以看出是不影响fill的,对非闭合图形执行fill,会帮你把图形闭合(从最后一点回到起点的一条路径),然后再填充。此处起点为上一次moveTo所在的点。对于stroke,绘制了多少线条就会描边多少线条,并不会自动闭合。

    图3中不执行beginPath的影响就大多了,存在两个问题:

    1.最左边的条状颜色偏深,越往右越浅

    2.最下面的刻度轴压住了除了最右的条状的所有条状(图上看不太出来,因为最右的颜色比较浅)

    为什么会这样呢?

    自然我们会从beginPath开始入手,beginPath的作用是重置当前路径,如果没有重置路径,那么这次的绘制是从哪里开始的呢?

    答案是从上次beginPath之后重新执行。

    如果绘制条状的时候没有beginPath,那么绘制过程就会如下:

    绘制起点是上一次beginPath的时候,也就是最后一条刻度轴的起点(绿色点)。

    绘制第n条的过程是这样的:

    1.从绿色点作为第一次起点,绘制一次刻度轴

    2.红色点作为第二次起点,绘制一次第一条条状

    3.依次绘制第二条、第三条,直到第n条

    因此绘制第n条的时候,第一条已经被重复绘制、填充了n次,第二条n-1次,...,以此类推。

    所以绘制完成6条,坐标轴被重复绘制了7次,第7次绘制时,盖住了前面绘制的条状,被后续绘制的条状盖住;第一条被重绘了6次,颜色叠加,因此颜色最深;第六条只绘制了1次,颜色最浅

    因此使用path的时候一定要注意使用beginPath和closePath。

    4.3 实现动画效果

    参考:https://m.html.cn/web/javascript/12369.html

    实现动画效果实际上是对部分区域的清除和重绘,每秒重绘24帧即可让人看起来这个动作是连续的,因此每秒能实现24帧及以上的重绘,即可实现动画效果。

    原理我们知道了,但是如何做到每秒刷新24次及以上,还需要一个函数的支持:window.requestAnimationFrame

    对此api的解释可以查看mdn:https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestAnimationFrame

      animate(to: any, index: number) {
        // 计算此时的y, h
        const h = Math.floor(to.h / this.frames * this.start[index]) + 0.5
        const y = this.y0 - h
        // 清除上一次绘制的矩形
        this.context.clearRect(to.x, y, to.w, h)
        // 重绘矩形
        this.context.fillRect(to.x, y, to.w, h)
        // 变化
        this.start[index]++
        if (this.start[index] <= this.frames) {
          window.requestAnimationFrame(() => this.animate(to, index))
        }
      }

    4.4 事件绑定

    在开始的canvas与svg的区别中,w3school列出了他们的差异之处,其中提到了canvas是不支持事件处理器的,canvas绘制完成后就成为一整块画布,不再引起浏览器的注意,我们无法对其中某个色块或者线段进行操作。

    因此如果想给某个色块添加事件,需要通过坐标的方式判断点击的是哪个色块。

      bindEvents() {
        // 给整个canvas绑定事件
        const canvas = document.getElementById('canvas') as HTMLCanvasElement
        // mousedown
        canvas.addEventListener('mousedown', this.mouseDownEvent.bind(this))
      }
    
      mouseDownEvent(e: any) {
        // 判断坐标
        const {offsetX, offsetY} = e
        // 对所有色块遍历,判断点击了哪个色块
        this.barList.map((bar, index) => {
          if (this.checkBoundary(offsetX, offsetY, bar.x, bar.y, bar.w, bar.h)) {
            console.log('you click this bar', bar)
          }
        })
      }
    
      checkBoundary(x0: number, y0: number, x1: number, y1: number, w: number, h: number) {
        // x0, y0为点击的坐标,x1,y1为色块左上角坐标,w,h为色块宽高
        return x0 > x1 && x0 < (x1 + w) && y0 > y1 && y0 < (y1 + h)
      }

    如果你的canvas绘制内容比较复杂,还需要考虑色块重叠时的情况。如果还需要做元素的拖拽、缩放、旋转、删除等,可能会更复杂。个人感觉对canvas的事件绑定远不如svg的简单。

    ---------------------end--------------------

    全部代码(vue+ts):

    <template>
      <div class="container">
        <span class="zero">0</span>
        <span class="x">x</span>
        <span class="y">y</span>
        <canvas id="canvas" :width="width" :height="height"></canvas>
      </div>
    </template>
    
    <script lang="ts">
    import { Component, Vue } from 'vue-property-decorator'
    
    @Component
    export default class Canvas extends Vue {
      // 视口
      width = 300
      height = 300
      // 字体大小
      fontsize = 14
      // 坐标轴
      minUnit = 10  // y轴最小刻度
      barWidth = 30 // 条状图宽度
      barList: any[] = [] // 条状图色块数据,坐标、宽、高
      barColor = 'rgb(200, 0, 0)'
      xAxis = ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子']    // x轴数据
      yAxis: number[] = []    // y轴数据
      // 动画
      start: number[] = []
      frames = 30
    
      // canvas上下文
      context: any
    
      get x0() {
        // 需要注意canvas的绘制问题
        // canvas绘制时会读取lineWidth,然后尝试在坐标处两边各绘制一般的lineWidth
        // 当lineWidth=1时,canvas会尝试在整数坐标处左右各绘制半个像素
        // 因为canvas无法在一个像素内绘制半个像素,因此坐标处上下两个方向都会被扩展至整个像素宽度内,即两个像素
        // 导致坐标处绘制1像素线条和2像素线条看起来时一样的 只是1像素线条颜色浅了一些
        // 解决办法是在想在n绘制1像素线条时,最好是n.5处绘制
        return 25.5  
      }
    
      get y0() {
        return this.height - 20.5
      }
    
      get xAxisLen() {
        return this.width - this.x0
      }
    
      get yAxisLen() {
        return this.y0 - 10.5
      }
    
      mounted() {
        // canvas创造了一个固定大小的画布,它公开了一个或多个渲染上下文
        // canvas与svg一样 存在视口的概念,不指定canvas标签的宽高属性时, 默认为300*150
        // 在内部样式中指定canvas画布的大小,而不指定视口大小,在实际中会影响到绘制效果
        const canvas: HTMLCanvasElement = document.getElementById('canvas') as HTMLCanvasElement
        // 获取2d上下文,没有这个方法则表示可能不支持canvas
        this.context =  canvas.getContext('2d')
    
        // 绘制辅助坐标轴
        this.drawSupport()
    
        // 绘制图表
        this.draw()
      }
    
      drawSupport() {
        // 开始绘制一段路径,并指定这段路径的样式
        this.context.beginPath()    // 起始一段路径,或重置当前路径
        this.context.moveTo(this.width, 0)
        this.context.lineTo(0, 0)
        this.context.lineTo(0, this.height)
        // this.context.closePath()
        this.context.strokeStyle = "#ff0000"
        this.context.stroke()
      }
    
      draw() {
        // 绘制坐标轴
        this.context.beginPath()
        this.context.moveTo(this.x0, this.y0 - this.yAxisLen)
        this.context.lineTo(this.x0, this.y0)
        this.context.lineTo(this.width, this.y0)
        // this.context.closePath()
        this.context.strokeStyle = '#000000'
        this.context.stroke()
    
        // x坐标轴
        this.context.font = `${this.fontsize}px Arial`
        const xGap = (this.width - this.x0) / this.xAxis.length
        this.xAxis.map((text, index) => {
          // 文本
          this.context.fillText(text, this.x0 + xGap * (index + 0.5) - text.length * this.fontsize / 2, this.height - 5)
          // 刻度
          this.context.beginPath()
          this.context.moveTo(this.x0 + xGap * (index + 1), this.y0)
          this.context.lineTo(this.x0 + xGap * (index + 1), this.y0 - 3)
          this.context.closePath()
          this.context.stroke()
        })
    
        // 模拟异步获取数据
        this.getAsyncData()
    
        // 绑定事件
        this.bindEvents()
      }
    
      getAsyncData() {
        setTimeout(() => {
          this.yAxis = [5, 20, 36, 10, 10, 20]
          // y坐标轴
          // 计算刻度
          const num = Math.ceil((Math.max(...this.yAxis)) / this.minUnit)
          const yGap = this.yAxisLen / num
          const max = num * this.minUnit
    
          // 绘制刻度及对齐轴
          for(let i = 0; i < num; i++) {
            const text = this.minUnit * (i + 1) + ''
            // 文本
            this.context.fillText(text, this.x0 - text.length * this.fontsize / 1.5, this.height - (10 + yGap * (i + 1) + this.fontsize / 2))
            // 刻度
            this.context.beginPath()
            const yUnit = Math.floor(this.y0 - this.yAxisLen + yGap * i) + 0.5
            this.context.moveTo(this.x0, yUnit)
            this.context.lineTo(this.x0 - 3, yUnit)
            this.context.strokeStyle = '#000000'
            this.context.stroke()
            // 对齐轴
            this.context.beginPath()
            this.context.moveTo(this.x0, yUnit)
            this.context.lineTo(this.width, yUnit)
            this.context.strokeStyle = '#eeeeee'
            this.context.stroke()
          }
    
          // 绘制条状图
          const xGap = (this.width - this.x0) / this.xAxis.length
          this.context.fillStyle = this.barColor
          this.barList = []
          this.start = new Array(this.yAxis.length).fill(0)   // 初始化帧数计数数组
          this.yAxis.map((data, index) => {
            const x1 = this.x0 + xGap * (index + 0.5) - this.barWidth / 2 
            const h = data / this.minUnit * yGap 
            const y1 = this.y0 - h
            const barData = {x: x1, y: y1, w: this.barWidth, h}
            // 保存色块数据
            this.barList.splice(index, 0, Object.assign({}, barData, {
              data,
              index,
              name: this.xAxis[index],
              color: this.barColor
            }))
            // 通过path绘制矩形
            // this.drawRectByPath(x1, y1, this.barWidth, h)
            // 添加动画效果
            this.animate(barData, index)
          })
        }, 1500);
      }
    
      animate(to: any, index: number) {
        // 计算此时的y, h
        const h = Math.floor(to.h / this.frames * this.start[index]) + 0.5
        const y = this.y0 - h
        // 清除上一次绘制的矩形
        this.context.clearRect(to.x, y, to.w, h)
        // 重绘矩形
        this.context.fillRect(to.x, y, to.w, h)
        // 变化
        this.start[index]++
        if (this.start[index] <= this.frames) {
          window.requestAnimationFrame(() => this.animate(to, index))
        }
      }
    
      drawRectByPath(x: number, y: number, w: number, h: number) {
        // 通过路径绘制矩形,x,y为左上角,w为宽,h为高
        // 如果不重置路径(beginPath)那么会从上一次的beginPath开始执行
        this.context.beginPath()
        this.context.moveTo(x, y)
        this.context.lineTo(x + w, y)
        this.context.lineTo(x + w, y + h)
        this.context.lineTo(x, y + h)
        // 如果不closePath,该路径则无法闭合,只会影响描边,不影响填充
        this.context.closePath()
        this.context.strokeStyle = 'rgba(0, 0, 0, 0.1)'
        this.context.fillStyle = 'rgba(200, 0, 0, 0.1)'
        // 未闭合路径也会自动回到开始路径并填充,但是stroke不会
        this.context.stroke()
        this.context.fill()
      }
    
      bindEvents() {
        // 给整个canvas绑定事件
        const canvas = document.getElementById('canvas') as HTMLCanvasElement
        // mousedown
        canvas.addEventListener('mousedown', this.mouseDownEvent.bind(this))
      }
    
      mouseDownEvent(e: any) {
        // 判断坐标
        const {offsetX, offsetY} = e
        // 对所有色块遍历,判断点击了哪个色块
        this.barList.map((bar, index) => {
          // todo: 如果图形有重叠,点击重叠区域,如何选中上层元素
          // 引入“层级”概念,为每个图形添加层级属性。越新创建的图形层级越高。
          // 点击重叠区域,直接选中层级高的元素
          // 点击非重叠的图形区域,将该图形的层级提到最高
          if (this.checkBoundary(offsetX, offsetY, bar.x, bar.y, bar.w, bar.h)) {
            console.log('you click this bar', bar)
          }
        })
      }
    
      checkBoundary(x0: number, y0: number, x1: number, y1: number, w: number, h: number) {
        // x0, y0为点击的坐标,x1,y1为色块左上角坐标,w,h为色块宽高
        return x0 > x1 && x0 < (x1 + w) && y0 > y1 && y0 < (y1 + h)
      }
    }
    </script>
    
    <style scoped>
    .container {
      position: relative;
    }
    .container span {
      position: absolute;
      color: red;
    }
    .zero {
      top: -10px;
      left: 15px;
    }
    .x {
      top: -10px;
      right: 10px;
    }
    .y {
      top: 100%;
      left: 25px;
    }
    
    #canvas {
       300px;
      height: 300px;
    }
    </style>
  • 相关阅读:
    从零开始搭建VUE项目
    推送类型
    spring整合消息队列rabbitmq
    Terracotta
    MYSQL INNODB 存储引擎
    Java 各种读取文件方法以及文件合并
    spring-security用户权限认证框架
    Spring Bean初始化过程
    使用PLSQL客户端登录ORACLE时报ORA-12502和ORA-12545错误的解决方案
    计算机语言基础概况
  • 原文地址:https://www.cnblogs.com/sue7/p/13280523.html
Copyright © 2020-2023  润新知