数据可视化案例

3,475 阅读3分钟

数据可视化案例总结

tip:项目用的Vue 其实React也差不多

补一个项目的bug,当数据中断后继续绘制会出现直线 如果不想要就判断断电 简单快速的方案,判断是否断点:先算出所有相邻两点差值的最小值min,如果某相邻两点的差值大于这个最小值min,则说明有断点,补一个null即可,从而不是连直线

Echarts

案例一

Echarts1

  • Echarts创建一个具备高宽的DOM容器
<div :id="'myChart' + idkey " :style="{width: '300px', height: '200px'}"></div>

id必须唯一,这样设置由于项目中有多个图表 单一的图表可以自己随便设置

  • 准备好初始化的内容 设置Echarts样式
      option: {
      // 直角坐标系 grid 中的 x 轴,一般情况下单个 grid 组件最多只能放上下两个 x 轴,多于两个 x 轴需要通过配置 offset 属性防止同个位置多个 x 轴的重叠。
        xAxis: {
          data: this.charttimes,// 类目数据
          show: false,// 是否显示 x 轴
          // 坐标轴刻度标签的相关设置
          axisLabel: {
            // 刻度标签的内容格式器,支持字符串模板和回调函数两种形式 
            formatter: function (value, idx) {// 函数参数分别为刻度数值(类目),刻度的索引
              let newDate = new Date(Number(value))
              return [newDate.getMonth() + 1, newDate.getDate()].join('/')
            },
            interval: 500// 坐标轴刻度标签的显示间隔,在类目轴中有效 设置为 1,表示『隔一个标签显示一个标签』,如果值为 2,表示隔两个标签显示一个标签,以此类推
          },
          boundaryGap: ['10%', '10%']// 坐标轴两边留白策略,类目轴和非类目轴的设置和表现不一样
        },
        // 类似xAxis
        yAxis: {
          show: false,
          boundaryGap: ['30%', '30%']
        },
        series: {
          showSymbol: false,// 是否显示 symbol, 如果 false 则只有在 tooltip hover 的时候显示
          hoverAnimation: false,// 是否开启 hover 在拐点标志上的提示动画效果
          data: this.chartusages,// 系列中的数据内容数组。数组项通常为具体的数据项
          type: 'line',// 图表类型
          lineStyle: {
            color: '#8C8C8C',
            width: 1
          }
        },
        // 提示框组件
        tooltip: {
          trigger: 'axis',// 触发类型 坐标轴触发
          formatter: function (datas) {// 提示框浮层内容格式器,支持字符串模板和回调函数两种形式
            let newDate = new Date(Number(datas[0].name))
            let dateFormatter = date2str(newDate, 'YYYY-MM-DD HH:mm:ss')
            return dateFormatter + '</br>' + datas[0].value + '%'
          },
          // 坐标轴指示器配置项
          axisPointer: {
            lineStyle: {
              color: '#3752ff'
            },
            shadowStyle: {
              color: '#3752ff'
            }
          }
        }
      }

一些项目中用到的配置都在上面 也有一些详细的解释 这里再提一下 提示框是指当鼠标移到数据部分会有一个框详细展示该点的信息 那么指示器就是一条线 上面配置就是一个颜色差不多深蓝色 平行于y轴的直线 随着鼠标的移动跟着动

  • 画图
 drawEChart () {
      获取DOM容器
      let myChart = echarts.init(document.getElementById(`myChart${this.idkey}`))
      this.option.xAxis.data = this.charttimes// 设置x轴数据 也就是说x轴应该显示啥 这就写啥
      this.option.series.data = this.chartusages // 设置图的数据 没数据拿啥画?
      myChart.setOption(this.option)// 画图
    }

案例二

Echarts

  • 创建容器

....这个代码就不写了

  • 准备配置
// 组件封装 数据传过来的 遍历一下 懒得改 凑合看
let series = this.ydata.map((data, idx) => ({
      name: this.names[idx],//系列名称
      data: data,//数据
      type: 'line',
      symbol: 'circle',
      showSymbol: false,
      /** 控制某点变色点时候不要直接设置颜色 */
      lineStyle: this.visualMap ? {} : {
        color: this.colors[idx]
      },
      itemStyle: {
        color: this.colors[idx],
        borderColor: this.colors[idx]
      },
      ...(this.area ? { areaStyle: typeof this.area === 'object' ? this.area : {
        color: new LinearGradient(0, 0, 0, 1, [{
          offset: 0,
          color: '#65BDF6'
        }, {
          offset: 1,
          color: this.colors[idx]
        }])
      }} : {})
    }))
    let that = this
    /** 控制某些点变颜色 */
    // visualMap 是视觉映射组件,用于进行『视觉编码』,也就是将数据映射到视觉元素(视觉通道)
    let visualMap = {
      type: 'piecewise',// 定义为分段型
      show: false,
      dimension: 0,
      pieces: this.pieces
    }
    Object.assign(this, {
      line: {
        title: {
          text: this.title
        },
        visualMap: that.visualMap ? visualMap : { show: false },
        tooltip: {
          trigger: 'axis',
          // textStyle: {
          //   color: '#0068FF'
          // },
          formatter: function (param) {
            return `${msec2str(Number(param[0].name), 'YYYY-MM-DD HH:mm:ss')}<br/>${param[0].value}`
          }
        },
        legend: {
          data: this.names
        },
        // 直角坐标系内绘图网格,单个 grid 内最多可以放置上下两个 X 轴,左右两个 Y 轴
        grid: {
          top: 40,
          bottom: 10,
          right: 25,
          left: 10,
          containLabel: true
        },
        xAxis: {
          data: this.xdata,
          type: 'category',
          axisTick: {
            alignWithLabel: true
          },
          axisLabel: {
            interval: that.xinterval || 59,
            showMaxLabel: true,
            formatter: function (value, idx) {
              // return idx === 0 ? msec2str(Number(value), 'YYYY-MM-DD') : msec2str(Number(value), 'MM-DD HH:mm')
              return msec2str(Number(value), that.xformatter || 'HH:00')
            }
          }
        },
        yAxis: {
          type: 'value'
        },
        series: series
      }
    })
    return {}
  • 画图也就不用我多说 (其实是我懒)

Dygraph

Dygraph

  • 创建一个DOM容器(代码跟前面的基本类似)

<div class="chart"></div>

  • 准备好配置文件
     plot () {
     // 有图就先删掉
      if (this.chart) {
        this.chart.destroy()
        this.chart = null
      }
      // 就是我们上面准备的容器
      this.chartEl.innerHTML = ''
      // 获取数据
      let data = await getData()
      if (data.values.length === 0) {
        this.chartEl.innerHTML = '<div class="chart-no-data">没有数据</div>'
        return
      }
      // 数据格式转换(不好意思 后端返的数据没法直接用 我先转换一下 各位看官可以跳过)
      let values = data.values.map(el => {
        let [a, b, c, , ...rest] = el
        return [a, b, c, ...rest]
      })
      let scoreValues = data.values.map(el => {
        // eslint-disable-next-line
        let [a, , , b, ...rest] = el
        return [a, b]
      })
      this.scoreValues = scoreValues
      // 上界和下界
      let lowers = []
      let uppers = []
      for (let v of values) {
        lowers.push([v[0], v[3]])
        uppers.push([v[0], v[4]])
        v[0] = msec2date(v[0])
        v[2] = v[2] ? v[1] : null
      }
      // tolerance update
      if (Object.keys(this.statistic).length > 0) {
        this.trimThreshold('upper', values, 4)
        this.trimThreshold('lower', values, 3)
        this.trimThreshold('upper', uppers, 1)
        this.trimThreshold('lower', lowers, 1)
      }
      // 样式指定
      let majorColor = '#136DFB'
      let minorColor = '#CCCCCC'
      let boundColor = '#408BFF'
      let anomalyColor = '#FF0000'
      let basebandColor = '#DCE9FE'
      let dylabels = [ 'x', '值', '异常' ]
      let series = {// 定义每系列选项。其键与y轴标签名称匹配,值是字典本身,包含特定于该系列的选项。
        '值': {
          color: majorColor,
          strokeWidth: 1,
          drawPoints: false
        },
        '异常': {
          color: anomalyColor,
          strokeWidth: 2,
          drawPoints: true,
          pointSize: 2,
          highlightCircleSize: 4
        },
        '分数': {
          color: majorColor,
          strokeWidth: 0,
          drawPoints: false,
          pointSize: 0,
          highlightCircleSize: 0
        }
      }
      dylabels = dylabels.concat([ '下界', '上界' ])
      series['上界'] = {
        color: boundColor,
        strokeWidth: 0,
        drawPoints: false,
        pointSize: 0,
        highlightCircleSize: 0
      }
      series['下界'] = {
        color: boundColor,
        strokeWidth: 0,
        drawPoints: false,
        pointSize: 0,
        highlightCircleSize: 0
      }

      // 是否填充
      if (this.filling) {
        let st = this.timerange.start
        if (dateLater(values[0][0], st)) {
          values.unshift([st, ...Array(values[0].length - 1).fill(null)])
        }
        let ed = this.timerange.end
        if (dateLater(ed, values[values.length - 1][0])) {
          values.push([ed, ...Array(values[0].length - 1).fill(null)])
        }
      }
      // 初始化dygraph
      this.chart = new Dygraph(this.chartEl, values, {
        labels: dylabels,//每个数据系列的名称,包括独立(X)系列
        connectSeparatedPoints: false,
        digitsAfterDecimal: 4,
        legend: 'follow',//何时显示图例。默认情况下,它仅在用户将鼠标悬停在图表上时显示
        interactionModel: Dygraph.defaultInteractionModel,
        labelsKMB: true,// 在y轴上显示千/千万/千亿的K / M / B.
        showRangeSelector: true,
        rangeSelectorHeight: 30,//范围选择器小部件的高度
        rangeSelectorPlotStrokeColor: majorColor,
        rangeSelectorPlotFillColor: minorColor,
        rangeSelectorPlotLineWidth: 1,
        rangeSelectorForegroundStrokeColor: minorColor,
        rangeSelectorForegroundLineWidth: 1,
        rangeSelectorBackgroundStrokeColor: minorColor,
        rangeSelectorBackgroundLineWidth: 1,
        axisLineColor: minorColor,
        gridLineColor: minorColor,
        series: series,
        axes: {//定义每轴选项
          x: {
            axisLabelFormatter: this.axisLabelFormatter
          },
          y: {
            axisLabelWidth: 35
          }
        },
        //设置后,每次用户通过鼠标悬停在图表外停止突出显示任何点时,都会调用此回调
        underlayCallback: (canvas, area, g) => {
          canvas.strokeStyle = basebandColor
          canvas.fillStyle = basebandColor
          let lowPoints = []
          let highPoints = []
          let drawBasebandBackgroundColor = () => {
            // 绘制基带 图中红色那一块
            // lowPoints对应下基带, hightPoints对应上基带
            // 至少有一边多于1个点才画: 才能画出线/区域
            if (lowPoints.length > 1 || highPoints.length > 1) {
              canvas.save()
              canvas.beginPath()
              if (lowPoints.length > 1) {
                canvas.moveTo(...lowPoints[0])
                // 从左到右
                for (let i = 1; i < lowPoints.length; i++) {
                  canvas.lineTo(...lowPoints[i])
                }
              }
              if (highPoints.length > 1) {
                if (lowPoints.length > 1) {
                  canvas.lineTo(...highPoints[highPoints.length - 1])
                } else {
                  canvas.moveTo(...highPoints[highPoints.length - 1])
                }
                // 从右到左
                for (let i = highPoints.length - 2; i >= 0; i--) {
                  canvas.lineTo(...highPoints[i])
                }
                if (lowPoints.length > 1) {
                  canvas.lineTo(...lowPoints[0])
                }
              }
              // 如果可以形成区域, 就填充
              if (lowPoints.length > 1 && highPoints.length > 1) {
                canvas.closePath()
                canvas.fill()
              }
              canvas.stroke()
              canvas.restore()
              lowPoints = []
              highPoints = []
            }
          }
          for (let j = 0; j < uppers.length; j++) {
            // 基带数据发生以下情况的变化, 就画出当前待绘制的这一段
            if (
              (lowPoints.length === highPoints.length && lowPoints.length > 0 && (lowers[j][1] === null || uppers[j][1] === null)) || // 2 -> 1/0
              (lowPoints.length > 0 && highPoints.length === 0 && lowers[j][1] === null) || // 1 -> 0
              (lowPoints.length === 0 && highPoints.length > 0 && uppers[j][1] === null) || // 1 -> 0
              (lowPoints.length > 0 && highPoints.length === 0 && lowers[j][1] !== null && uppers[j][1] !== null) || // 1 -> 2
              (lowPoints.length === 0 && highPoints.length > 0 && lowers[j][1] !== null && uppers[j][1] !== null) // 1 -> 2
            ) {
              drawBasebandBackgroundColor()
            }
            let xx = 0
            if (lowers[j][1] !== null) {
              let x = g.toDomXCoord(lowers[j][0])
              let y = g.toDomYCoord(lowers[j][1])
              lowPoints.push([x, y])
              xx = x
            }
            if (uppers[j][1] !== null) {
              let x = g.toDomXCoord(uppers[j][0])
              let y = g.toDomYCoord(uppers[j][1])
              highPoints.push([x, y])
              xx = x
            }
            if (xx >= area.x + area.w) {
              break
            }
          }
          drawBasebandBackgroundColor()
        }
      })
    }
  • 开始画图
this.chartEl = this.$el.getElementsByClassName('chart')[0]
this.plot()

D3

d3业务写的有点复杂 下次写

总结

大概平时项目就用过这三种可视化库,每个都有自己的特点 具体用什么还得看项目 欢迎各位跟我一起讨论数据可视化