canvas实现睡眠波2.0,框架兼容,支持大部分PC端

撰写中,npm暂未发布

npm引入方式

npm i @JayVone/sleepWave

// 使用方式
import { sleepWindow } from '@JayVone/sleepWave'

// 获取dom然后绑定
const app = document.getElementById('app');
sleepWindow.init(app);

// 在需要绘制的时候setOptions
sleepWindow.setOption({
  types: ['1', '2', '3', '4'],
  data: [
    { label: 'test', value: 22 },
    { label: '1', value: 12},
    { label: '2', value: 1 },
  ],
})

基本效果

更新说明

  • 老版本的代码是用vue2写了tooltip,导致很多码友找我要兼容其他框架的demo,然后感觉自己这个demo明明是仿echarts风格写的,却完全不灵活,纯属是个半成品,所以回炉重造了一下。
  • 更新内容:
    • 写法更新,纯js,只依赖lodash,无多余累赘内容,应该能兼容大部分框架,但是由于有大量window相关操作,所以小程序和app应该暂时不兼容,uniapp应该支持。有问题、有bug都可以cue我,如果想要加入我一起维护,更是求之不得。
    • 支持自定义,包括:Y轴label、文字对齐、分类;X轴刻度、formatter自定义、最小值、最大值、splitNumber;线宽;块高、弧度、点击事件;颜色;动画;tooltip自定义;
    • 异常处理做得不是很完善,使用时注意边界值、异常值

代码

// index.js
import { SleepWave } from './sleepWave.js';

export const sleepWindow = new SleepWave();

window.$sleepWindow = sleepWindow;
// canvasEvent.js
class Event {
  constructor () {
    this._listener = {}
  }

  /**
   * 监听
   */
  on (type, handler) {
    if (!this._listener[type]) {
      this._listener[type] = []
    }

    this._listener[type].push(handler)
  }

  /**
   *触发
   */
  emit (type, event) {
    if (event == null || event.type == null) {
        return
    }
    const typeListeners = this._listener[type]
    if (!typeListeners) return
    for (let index = 0; index < typeListeners.length; index++) {
      const handler = typeListeners[index]
      handler(event)
    }
  }

  /**
   * 删除
   */
  remove (type, handler) {
    if (!handler) {
      this._listener[type] = []
      return
    }

    if (this._listener[type]) {
      const listeners = this._listeners[type]
      for (let i = 0, len = listeners.length; i < len; i++) {
          if (listeners[i] === handler) {
              listeners.splice(i, 1)
          }
      }
    }
  }
}
export default Event
// sleepWave.js
import { SleepWaveBlock } from './sleepBlock'
import { SleepWaveLine } from './sleepLine'
import { Tooltip } from './tooltip'
import { throttle, isArray, isObject, uniq, isUndefined } from 'lodash'
import { randomColor, getIdealTenTimesNum } from './tool/utils'

export class SleepWave {
  $el = null
  canvas = null
  ctx = null
  tooltip = null
  eventList = ['click', 'mousemove']
  children = [] // 存放子元素

  // 图表显示相关主要数据
  types = ['深睡', '浅睡', '眼动', '清醒']
  colors = ['#FFB9E4', '#D3B4FF', '#9345FF', '#5701CD']
  data = []

  // 可配置基本属性
  axisColor = '#EAEAEA'
  axisLabelColor = '#EAEAEA'
  axisWidthY = 50 // Y轴的宽度
  axisHeightX = 20 // X轴的高度
  paddingRight = 10 // 右侧留出的显示空间宽度
  blockHeight = 0 // 方块的高度,不配置就默认取disY - 4 * borderRadius
  borderRadius = 12 // 方形弧度
  lineWidth = 2 // 连线宽度
  labelAlign = 'right' // Y轴label文字对齐 left or right
  showAxisLabelX = false // 是否显示X轴坐标刻度
  maxX = 800 // X轴最大值
  minX = 0 // X轴最小值
  splitNumber = 5 // X轴刻度分割数
  markHeightX = 5 // X轴刻度高度
  animation = true // 是否开启动画,默认开启
  drawerFrequency = 20 // 动画帧数,决定动画快慢,默认20帧
  tooltipFormatter = null // 自定义tooltip
  xAxisFormatter = null // 自定义x轴label
  xAxisMarkVisible = true // 是否显示X轴刻度
  yAxisLabelVisible = true // 是否显示Y轴type

  // 不可配置的属性
  disX = 1 // 单位x的宽度
  disY = 1 // 单位y的高度

  // 初始化
  init(dom) {
    this.$el = dom
    this.$el.classList.add('sleep-wave-canvas')
    document.styleSheets[0].insertRule('.sleep-wave-canvas { position: relative; overflow: auto }', 0);
    document.styleSheets[0].insertRule('.sleep-wave-canvas::-webkit-scrollbar { display:none }', 1);
    this.canvas = document.createElement('canvas')
    this.canvas.width = this.$el.clientWidth
    this.canvas.height = this.$el.clientHeight
    this.$el.appendChild(this.canvas)
    this.ctx = this.canvas.getContext('2d')
  }

  initTooltip() {
    this.tooltip = new Proxy(new Tooltip(this.$el, this.tooltipFormatter), {
      get: function (obj, prop) {
        return prop in obj ? obj[prop] : undefined
      },
      set: function (obj, prop, value) {
        if (prop === 'visible') {
          if (value && !obj.visible) {
            obj.show()
          }
          if (!value && obj.visible) {
            obj.hide()
          }
        }

        obj[prop] = value

        if (prop === 'blockInfo') {
          obj.updateSlotDom()
        }
        return true
      }
    })
  }

  // 设置option
  setOption(option) {
    // 清空整个画板
    this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
    // 自定义属性
    const { data, types, colors, tooltipFormatter, xAxisFormatter } = option
    this.tooltipFormatter = tooltipFormatter
    this.xAxisFormatter = xAxisFormatter
    this.setData(data, types, colors)
    this.initTooltip()

    this.borderRadius = option.borderRadius || this.borderRadius
    this.lineWidth = option.lineWidth || this.lineWidth
    this.splitNumber = option.splitNumber || this.splitNumber
    this.labelAlign = option.labelAlign || this.labelAlign
    this.markHeightX = option.markHeightX || this.markHeightX
    this.axisWidthY = option.axisWidthY || this.axisWidthY
    this.axisHeightX = option.axisHeightX || this.axisHeightX
    this.paddingRight = option.paddingRight || this.paddingRight
    this.blockHeight = option.blockHeight || this.blockHeight
    this.drawerFrequency = option.drawerFrequency || this.drawerFrequency
    this.animation = isUndefined(option.animation) ? this.animation : option.animation
    this.xAxisMarkVisible = isUndefined(option.xAxisMarkVisible) ? this.xAxisMarkVisible : option.xAxisMarkVisible
    this.yAxisLabelVisible = isUndefined(option.yAxisLabelVisible) ? this.yAxisLabelVisible : option.yAxisLabelVisible
    const dataMax = this.data.reduce((sum, item) => sum + item[0], 0)
    this.maxX = option.maxX || getIdealTenTimesNum(dataMax, this.splitNumber)

    // 根据maxX的字符长度增加paddingRight
    let maxXWidth = ''
    if (typeof this.xAxisFormatter === 'function') {
      maxXWidth = this.ctx.measureText(String(this.xAxisFormatter(this.maxX))).width
    } else {
      maxXWidth = this.ctx.measureText(String(this.maxX)).width
    }
    this.paddingRight += maxXWidth

    // 更新相关属性
    this.updateProperties()
    // 开始绘制
    this.draw()
    // 初始化事件
    this.initEvent()
  }

  // 根据data类型取值
  setData(data, types, colors) {
    if (types) {
      this.types = types
    }
    if (colors) {
      this.colors = colors
    }
    if (data.length <= 0) return

    // 如果数组元素的类型也是数组,则用第一位做时长,第二位做Y轴类型(0开始的整数)
    if (isArray(data[0])) {
      // 没传types则为index
      if (!types) {
        this.types = uniq(data.map(item => item[1]))
      }
      this.data = data.map(item => ([
        item[0],
        this.types.indexOf(item[1])
      ])).filter(item => this.types.indexOf(item[1]) > -1)
    }

    // 如果数组元素的类型是对象,则用 { label, value } 来做类型,并更新types
    else if (isObject(data[0])) {
      // 没传types则根据数据中包含的label来确定types
      if (!types) {
        this.types = uniq(data.map(item => item.label))
      }
      this.data = data.map(item => ([
        item.value,
        this.types.indexOf(item.label),
      ])).filter(item => item[1] > -1)
    }
    else {
      throw new TypeError('data is not valid in setOption')
    }

    if (this.colors.length < this.types.length) {
      for (let i = this.colors.length; i < this.types.length; i ++) {
        this.colors[i] = randomColor()
      }
    }
  }

  // 更新相关属性
  updateProperties() {
    this.disX = (this.canvas.width - this.axisWidthY - this.paddingRight) / (this.maxX - this.minX)
    this.disY = (this.canvas.height - this.axisHeightX) / this.types.length
    this.blockHeight = this.blockHeight || (this.disY - 4 * this.borderRadius)
    // borderRadius最大为块高度的一半也就是绘图区高度的1/8
	  this.borderRadius = Math.min(this.disY / 4, this.borderRadius)
  }

  // canvas事件
  initEvent() {
    this.eventList.forEach(eventName => {
      // 高频率事件需要防抖
      this.canvas.addEventListener(eventName, throttle(this.handleEvent, 40))
    })
  }

  handleEvent = (event) => {
    this.children
    .filter(shape => shape.isEventInRegion(event.x, event.y))
    .forEach(shape => shape.emit(event.type, event))
  }

  // 绘制
  draw() {
    // 绘制坐标
    this.drawAxis()
    // 绘制Y轴刻度
    this.drawAxisYLabel()
    // 绘制X轴刻度和值
    this.drawAxisXMark()
    // 绘制波形
    this.drawWave()
  }

  // 绘制波形
  drawWave() {
    this.data.reduce((pre, item) => {
      // 先画方形自己
      // 方框四个点位置
      const startX = pre ? pre[0] : this.axisWidthY
      const endX = startX + item[0] * this.disX
      const startY = (this.types.length - item[1] - 0.5) * this.disY - this.borderRadius - this.blockHeight / 2
      const endY = startY + this.blockHeight + 2 * this.borderRadius
      // 表示宽度是否够画直线,如果不够直接连着贝塞尔
      const hasStraight = (endX - startX) > 2 * this.borderRadius
      // 够不够画直线都要计算那个点的位置,X轴差值
      const nowRadius = hasStraight ? this.borderRadius : (endX - startX) / 2
      const { colors, types } = this

      // block块对象
      const block = new SleepWaveBlock(this, {
        startX,
        startY,
        endX,
        endY,
        nowRadius,
        value: item[0],
        title: types[item[1]],
        color: colors[item[1]],
      })
      block.draw()
      this.children.push(block)

      // 再连线
      if (pre && pre[1] !== item[1]) { // 一样高得情况不画线
        const line = new SleepWaveLine(this, {
          pre,
          startX,
          startY,
          endY,
          nowType: item[1],
          nowRadius,
        })
        line.draw()
      }
      return [endX, item[1], nowRadius] // 下一个pre为当前点的endx和item[1],注意不是x不是item[0], 这里的第三个是上一次绘图的弧度
    }, null)
  }

  // 绘制坐标
  drawAxis() {
    this.ctx.beginPath()
    this.ctx.lineWidth = 0.5
    this.ctx.strokeStyle = this.axisColor
    this.ctx.moveTo(this.axisWidthY - 2, 0)
    this.ctx.lineTo(this.axisWidthY - 2, this.canvas.height - this.axisHeightX)
    this.ctx.lineTo(this.canvas.width - this.paddingRight, this.canvas.height - this.axisHeightX)
    this.ctx.stroke()
  }

  // 绘制Y轴type
  drawAxisYLabel() {
    this.ctx.font = '14px PingFang SC'
    this.ctx.fillStyle = this.axisLabelColor
    if (!this.yAxisLabelVisible) {
      return
    }
    const typesLen = this.types.length
    this.types.forEach((label, index) => {
      const textWidth = this.ctx.measureText(label).width
      // 问题过长还要省略显示
      if (textWidth > this.axisWidthY - 12) {
        let temp = ''
        const ellipseWidth = this.ctx.measureText('...').width
        for (let i = 0; i < label.length; i ++) {
          if (this.ctx.measureText(label.slice(0, i + 1)).width + ellipseWidth <= this.axisWidthY - 12) {
            temp = label.slice(0, i + 1)
          }
        }
        const labelX = this.labelAlign === 'left' ? 4 : this.axisWidthY - this.ctx.measureText(temp + '...').width - 8
        this.ctx.fillText(temp + '...', labelX, this.disY * (typesLen - index - 0.5))
      } else {
        const labelX = this.labelAlign === 'left' ? 4 : this.axisWidthY - textWidth - 8
        this.ctx.fillText(label, labelX, this.disY * (typesLen - index - 0.5))
      }
    })
  }

  // 绘制X轴刻度和值
  drawAxisXMark() {
    const markStartX = this.axisWidthY - 2, markY = this.canvas.height - this.axisHeightX - 1
    const markDis = (this.canvas.width - markStartX - this.paddingRight) / this.splitNumber
    const markValDis = (this.maxX - this.minX) / this.splitNumber
    
    this.ctx.font = '12px PingFang SC'
    this.ctx.fillStyle = this.axisLabelColor
    // 最小刻度值先画
    const minText = String(this.minX)
    const minTextWidth = this.ctx.measureText(minText).width
    this.ctx.fillText(minText, markStartX - minTextWidth / 2, markY + 14)

    for (let i = 1; i <= this.splitNumber; i ++) {
      // 是否绘制刻度
      if (this.xAxisMarkVisible) {
        this.ctx.beginPath()
        this.ctx.lineWidth = 0.5
        this.ctx.strokeStyle = this.axisColor
        this.ctx.moveTo(markStartX + i * markDis - 1, markY)
        this.ctx.lineTo(markStartX + i * markDis - 1, markY - this.markHeightX)
        this.ctx.stroke()
      }
      
      this.ctx.font = '12px PingFang SC'
      this.ctx.fillStyle = this.axisLabelColor
      let markText = ''
      if (typeof this.xAxisFormatter === 'function') {
        markText = String(this.xAxisFormatter(this.minX + i * markValDis))
      } else {
        markText = String(this.minX + i * markValDis)
      }
      const markTextWidth = this.ctx.measureText(markText).width
      this.ctx.fillText(markText, markStartX + i * markDis - markTextWidth / 2 - 1, markY + 14)
    }
  }
}
// sleepBlock.js
import Event from './canvasEvent'

export class SleepWaveBlock extends Event {
  _parent = null
  point = { x: 0, y: 0 }
  mainWidth = 0 // 画布宽度
  mainHeight = 0 // 画布高度
  position = '' // tooltip是否固定定位,left || right || top || bottom,没有则是跟随鼠标移动
  
  // 动画属性
  drawerFrequency = 20 // 20帧动画
  drawerIndex = 0
  drawerSeg = 0
  
  constructor (canvas, opts) {
    super()
    this._parent = canvas
    this.ctx = canvas.ctx
    this.mainWidth = this._parent.canvas.width
    this.mainHeight = this._parent.canvas.height

    // 基本属性直接由构造器传进来
    Object.assign(this, opts)
    this.on('click', this.handleClick)
    this.on('mousemove', this.handleMouseMove)
  }

  commonHandler = () => {
    document.body.style.cursor = 'pointer'
    this._parent.tooltip.visible = true
  }

  // 点击事件自定义
  handleClick = (e) => {
    this.commonHandler()
  }

  // 移动事件
  handleMouseMove = (e) => {
    this.commonHandler()
    setTimeout(() => { // tooltip刚显示时还获取不到$el,下一帧再处理数据
      this.setPosX()
      this.setPosY()
      this._parent.tooltip.blockInfo = {
        color: this.color,
        title: this.title,
        value: this.value,
      }
    })
  }

  // 确认x位置
  setPosX() {
    const { tooltip, axisWidthY } = this._parent
    const boxWidth = this._parent.tooltip.$el.clientWidth
    if (this.position === 'left') {
      tooltip.position.x = axisWidthY
    } else if (this.position === 'right') {
      tooltip.position.x = this.mainWidth - boxWidth - 12
    } else {
      tooltip.position.x = this.point.x + boxWidth + 32 >= this.mainWidth ?
        this.mainWidth - boxWidth - 12 :
        this.point.x + 20
    }
  }

  // 确认y位置
  setPosY() {
    const { tooltip, axisHeightX } = this._parent
    const boxHeight = this._parent.tooltip.$el.clientHeight
    if (this.position === 'top') {
      tooltip.position.y = 8
    } else if (this.position === 'bottom') {
      tooltip.position.y = this.mainHeight - axisHeightX - boxHeight
    } else {
      tooltip.position.y = this.point.y - boxHeight - 12 < 0 ? 8 : this.point.y - boxHeight - 8
    }
  }

  draw () {
    const { startX, endX, nowRadius } = this
    const { drawerFrequency, animation } = this._parent
    this.drawerIndex = animation ? 0 : drawerFrequency
    this.drawerSeg = (endX - startX - 2 * nowRadius) / drawerFrequency / 2
    this.drawOneTime()
  }

  drawOneTime() {
    const { drawerFrequency, borderRadius } = this._parent
    if (this.drawerIndex > drawerFrequency) {
      return
    }

    const { startY, endY, nowRadius } = this
    // 表示宽度是否够画直线,如果不够直接连着贝塞尔
    const dis = (drawerFrequency - this.drawerIndex) * this.drawerSeg
    const startX = this.startX + dis
    const endX = this.endX - dis
    const hasStraight = (endX - startX) > 2 * borderRadius

    this.ctx.beginPath()
    this.ctx.lineWidth = this._parent.lineWidth
    this.ctx.strokeStyle = this.color

    // left border
    this.ctx.moveTo(startX, startY + borderRadius)
    this.ctx.lineTo(startX, endY - borderRadius)
    this.ctx.quadraticCurveTo(startX, endY, startX + nowRadius, endY)

    // bottom border
    if (hasStraight) { // 是否需要画直线
      this.ctx.lineTo(endX - borderRadius, endY)
    }

    // right border
    this.ctx.quadraticCurveTo(endX, endY, endX, endY - borderRadius)
    this.ctx.lineTo(endX, startY + borderRadius)
    this.ctx.quadraticCurveTo(endX, startY, endX - nowRadius, startY)

    // top border
    if (hasStraight) { // 是否需要画直线
      this.ctx.lineTo(startX + borderRadius, startY)
    }
    this.ctx.quadraticCurveTo(startX, startY, startX, startY + borderRadius)

    this.ctx.stroke()
    this.ctx.fillStyle = this.color
    this.ctx.fill()

    this.drawerIndex += 1
    requestAnimationFrame(() => this.drawOneTime())
  }

  // 事件触发的位置是不是当前block位置
  isEventInRegion (clientX, clientY) {
    this.point = this.getEventPosition(clientX, clientY) // 计算基于canvas坐标系的坐标值
    const { x, y } = this.point
    const width = this.endX - this.startX
    const height = this.endY - this.startY
    if (this.startX < x && x < this.startX + width && this.startY < y && y < this.startY + height) {
      return true
    }
    document.body.style.cursor = 'default'
    this._parent.tooltip.visible = false
    return false
  }

  getEventPosition (clientX, clientY) {
    const box = this._parent.canvas.getBoundingClientRect()
    return {
      x: clientX - box.left,
      y: clientY - box.top
    }
  }
}
// sleepLine.js
export class SleepWaveLine {
  _parent = null
  // 动画属性
  drawerIndex = 0
  drawerSeg = 0 // 绘制直线时Y轴每次绘制长度
  linearGradient = '' // 渐变色
  lineX = 0 // 连线X位置
  lineStartY = 0 // 最终的Y位置
  lineEndY = 0 // 最终的Y位置
  dir = true // 0: 一样高(这种情况理论不应该存在),true: 从下到上,false:从上到下
  preCurveSegX = 0 // 与上一个block连接的三角,绘制时的X轴方向每次绘制长度
  curveSegX = 0 // 当前block绘制三角时X轴方向每次绘制长度
  curveSegY = 0 // 绘制三角时Y轴方向每次绘制长度
  idealCurveRadius = 0 // 理想的圆弧
  
  constructor (canvas, opts) {
    this._parent = canvas
    this.ctx = canvas.ctx
    Object.assign(this, opts)
  }

  draw () {
    const { startX, startY, endY, pre, nowType, nowRadius } = this
    const { disY, types, blockHeight, animation, drawerFrequency, borderRadius, colors } = this._parent
    this.drawerIndex = animation ? 0 : drawerFrequency
    this.lineX = startX // 连线X位置
    
    // 需要判断是从下到上还是从上到下,注意canvas Y轴是上小下大,这里的上下是视觉的上下
    this.dir = pre[1] < nowType // 0: 一样高(这种情况理论不应该存在),true:从下到上,false:从上到下
    // start到end是从左到右
    this.lineStartY = this.dir ?
      ((types.length - pre[1] - 0.5) * disY - borderRadius - blockHeight / 2) :
      ((types.length - pre[1] - 0.5) * disY + borderRadius + blockHeight / 2) // 连线起点Y
    this.lineEndY = this.dir ? endY : startY// 连线终点Y
    this.linearGradient = this.ctx.createLinearGradient(this.lineX, this.lineStartY, this.lineX, this.lineEndY) // 线的渐变色
    this.linearGradient.addColorStop(0, colors[pre[1]])
    this.linearGradient.addColorStop(1, colors[nowType])

    this.drawerSeg = Math.abs(this.lineEndY - this.lineStartY) / drawerFrequency / 2
    this.preCurveSegX = pre[2] / drawerFrequency
    this.curveSegX = nowRadius / drawerFrequency
    this.curveSegY = borderRadius / drawerFrequency
    this.idealCurveRadius = (disY - blockHeight) / 2
    // 先画直线
    this.drawLineOneTime()
  }

  // 绘制直线
  drawLineOneTime() {
    const { drawerFrequency, borderRadius, animation } = this._parent
    if (this.drawerIndex > drawerFrequency) {
      // 直线画完了,重置drawerIndex开始画曲线
      this.drawerIndex = animation ? 0 : drawerFrequency
      this.drawCurveLine()
      return
    }

    const dis = this.drawerSeg * this.drawerIndex
    const startY = (this.lineStartY + this.lineEndY) / 2 + (this.dir ? dis : -dis)
    const endY = (this.lineStartY + this.lineEndY) / 2 + ((this.dir ? -dis : dis))
    
    this.ctx.beginPath()
    this.ctx.lineWidth = this.lineWidth
    this.ctx.strokeStyle = this.linearGradient
    this.ctx.moveTo(this.lineX, startY)
    this.ctx.lineTo(this.lineX, endY)
    this.ctx.stroke()

    this.drawerIndex += 1
    requestAnimationFrame(() => this.drawLineOneTime())
  }

  // 绘制曲线
  drawCurveLine() {
    const { drawerFrequency, animation, borderRadius, lineWidth } = this._parent
    if (this.drawerIndex > drawerFrequency) {
      return
    }
    const { lineX, pre, dir, nowRadius, lineStartY, lineEndY, preCurveSegX, curveSegX, curveSegY, linearGradient, idealCurveRadius } = this

    // start左三角
    const curveStartLineX = lineX + lineWidth / 2
    this.ctx.beginPath()
    this.ctx.lineWidth = lineWidth / 2
    this.ctx.strokeStyle = linearGradient
    this.ctx.moveTo(curveStartLineX, lineStartY + (dir ? -idealCurveRadius  : idealCurveRadius))
    this.ctx.quadraticCurveTo(
      curveStartLineX, lineStartY + (dir ? -lineWidth / 2 : lineWidth / 2),
      curveStartLineX - preCurveSegX * this.drawerIndex, lineStartY + (dir ? -lineWidth / 2 : lineWidth / 2)
    )
    this.ctx.quadraticCurveTo(
      curveStartLineX, lineStartY + (dir ? curveSegY * this.drawerIndex / 2 : -(curveSegY * this.drawerIndex) / 2),
      curveStartLineX, lineStartY + (dir ? curveSegY * this.drawerIndex : -(curveSegY * this.drawerIndex))
    )
    this.ctx.closePath()
    this.ctx.fillStyle = linearGradient
    this.ctx.fill()

    // end右三角
    const curveEndLineX = lineX - lineWidth / 2
    this.ctx.beginPath()
    this.ctx.lineWidth = lineWidth / 2
    this.ctx.strokeStyle = linearGradient
    this.ctx.moveTo(curveEndLineX, lineEndY + (dir ? idealCurveRadius : -idealCurveRadius))
    this.ctx.quadraticCurveTo(
      curveEndLineX, lineEndY + (dir ? lineWidth / 2 : -lineWidth / 2),
      curveEndLineX + curveSegX * this.drawerIndex, lineEndY + (dir ? lineWidth / 2 : -lineWidth / 2)
    )
    this.ctx.quadraticCurveTo(
      curveEndLineX, lineEndY + (dir ? -curveSegY * this.drawerIndex / 2 : curveSegY * this.drawerIndex / 2),
      curveEndLineX, lineEndY + (dir ? -curveSegY * this.drawerIndex : curveSegY * this.drawerIndex)
    )
    this.ctx.closePath()
    this.ctx.fillStyle = linearGradient
    this.ctx.fill()
    
    this.drawerIndex += 1
    requestAnimationFrame(() => this.drawCurveLine())
  }
}
// tooltip.js
import { isNumber, isObject } from 'lodash'

export class Tooltip {
  labels = []
  values = []
  parentDom = null
  visible = false
  blockInfo = {}

  // 插槽dom
  slotDom = null
  // tooltip最外层dom及其位置
  $el = null
  position = new Proxy({ x: 0, y: 0 }, {
    get: function (obj, prop) {
      return prop in obj ? obj[prop] : undefined
    },
    set: (obj, prop, value) => {
      if (prop !== 'x' && prop !== 'y') {
        throw new TypeError(`${prop} is not valid property`)
        return false
      }
      if (!isNumber(value)) {
        throw new TypeError(`${value} is not a number`)
        return false
      }
      obj[prop] = value
      this.$el.style.cssText = `display: ${this.visible ? 'block' : 'none'}; position: absolute; left: ${obj.x}px; top: ${obj.y}px;`
      return true
    },
  })

  constructor(parent, formatter) {
    this.$el = document.createElement('div')
    // 允许自定义tooltip
    if (typeof formatter === 'function') {
      this.updateSlotDom = () => {
        this.$el.innerHTML = formatter(this.blockInfo)
      }
    } else {
      this.slotDom = this.defaultTooltip()
      this.updateSlotDom()
      this.$el.appendChild(this.slotDom)
    }
    this.parentDom = parent
    this.parentDom.appendChild(this.$el)
    this.$el.style.cssText = 'display: none;'
  }

  defaultTooltip() {
    const dom = document.createElement('div')
    dom.style.cssText = `
      background-color: white;
      padding: 10px 8px;
      border-radius: 6px;
      box-shadow: 3px 3px 3px 3px rgba(0, 0, 0, 0.15);
    `
    return dom
  }

  // 刷新slotDom
  updateSlotDom() {
    const { color, title, value } = this.blockInfo
    this.slotDom.style.border = `2px solid ${color || 'black'}`
    this.slotDom.innerHTML = `
      <div style="font-size: 14px; white-space: nowrap;">
        ${title || '--'}:
        <span style="font-weight: bold; color: ${color};">${value || '--'}</span>
      </div>
    `
  }

  show() {
    this.$el.style.cssText = this.$el.style.cssText.replace('display: none', 'display: block')
  }

  hide () {
    this.$el.style.cssText = this.$el.style.cssText.replace('display: block', 'display: none')
  }
}
// tool/utils.js
export function randomColor (type = 'hex') {
  const chars = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f']

  switch(type) {
    // #000000型
    case 'hex': 
      const temp1 = new Array(6).fill(0)
      return '#' + temp1.map(item => {
        const randomIndex = Math.floor(Math.random() * 16)
        return chars[randomIndex]
      }).join('')

    // #00000000型
    case 'hexo':
      const temp2 = new Array(8).fill(0)
      return '#' + temp2.map(item => {
        const randomIndex = Math.floor(Math.random() * 16)
        return chars[randomIndex]
      }).join('')

    // rgb(255,255,255)型
    case 'rgb':
      const temp3 = new Array(3).fill(0)
      return 'rgb(' + temp3.map(item => {
        return Math.floor(Math.random() * 255)
      }).join(',') + ')'

    // rgba(255,255,255,1)型
    case 'rgba':
      const temp4 = new Array(3).fill(0)
      const randomOpacity = Math.random().toFixed(2)
      const rgb = temp4.map(item => {
        return Math.floor(Math.random() * 255)
      }).join(',')
      return `rgba(${rgb},${randomOpacity})`
  }
}

export function getIdealTenTimesNum (max, split) {
  const str = String(max)
  // 幂次计算
  const e = Number(str[0]) < 8 ? str.length - 1 : str.length
  const inc = Math.pow(10, e) // 取10的e次幂
  let ideal = inc
  while (ideal % split !== 0 || ideal < max) { // 如果ideal不能整除split,则继续增加ideal
    ideal += inc
  }
  return ideal
}
// webpack.config.js
const path = require('path');

module.exports = {
  mode: 'development',
  // 入口,是一个对象
  entry: {
    sleepWave: './src/index.js'
  },
  // 输出
  output: {
    filename: 'index.js',
    path: path.resolve(__dirname, './sleepWave'),
    library: {
      name: 'sleepWave',
      type: 'umd',
    },
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /(node_moduless)/, // 排除掉node_module目录
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env']
          }
        }
      },
      {
        test: /.css$/,
        use: [
          'style-loader', // 将css-loader转换后的结果放到style标签里面
          'css-loader'    // 先执行css-loader,而且是从后往前执行,所以需要放到下面 
        ]
      }
    ]
  },
}
// package.json
{
  "name": "sleep-wave",
  "version": "1.0.0",
  "description": "#### 介绍 睡眠波",
  "main": "sleepWave/index.js",
  "scripts": {
    "dev": "webpack-dev-server --config webpack.config.js",
    "build": "webpack --config webpack.config.js"
  },
  "author": "Mizuki",
  "license": "ISC",
  "dependencies": {
    "lodash": "^4.17.21"
  },
  "devDependencies": {
    "@babel/core": "^7.15.5",
    "@babel/plugin-transform-runtime": "^7.15.0",
    "@babel/polyfill": "^7.12.1",
    "@babel/preset-env": "^7.15.6",
    "babel-core": "^6.26.3",
    "babel-eslint": "^10.1.0",
    "babel-loader": "^8.2.2",
    "eslint": "^8.16.0",
    "eslint-plugin-vue": "^9.0.1",
    "webpack": "^5.52.1",
    "webpack-cli": "^4.8.0",
    "webpack-dev-server": "^4.2.1",
    "webpack-obfuscator": "^3.5.1"
  }
}
// index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <script src="./sleepWave/index.js"></script>
  <style>
    html{
      background-color: #222;
    }
    #app{
      width: 100%;
      height: 500px;
    }
  </style>
</head>
<body>
  <div id="app"></div>
  <script>
    const app = document.getElementById('app');
    $sleepWindow.init(app);
    $sleepWindow.setOption({
      types: ['1', '2', '3', '4', 'xixixixixixixixixixi', 'test'],
      data: [
        { label: 'test', value: 22 },
        { label: '1', value: 12},
        { label: '2', value: 1 },
        { label: '3', value: 2},
        { label: 'xixixixixixixixixixi', value: 5 },
        { label: 'shs', value: 3 },
        { label: '2', value: 12},
        { label: '2', value: 22},
        { label: 'shasds', value: 3 },
      ],
      borderRadius: 16,
      blockHeight: 2,
      // animation: false,
      // axisWidthY: 10,
      // yAxisLabelVisible: false,
      // xAxisMarkVisible: false,
      // xAxisFormatter: (val) => {
      //   return val * 100000000000000
      // }
      // tooltipFormatter: (blockInfo) => {
      //   return `
      //     <div style="background-color: white;">
      //       ${blockInfo.title}
      //       ${blockInfo.value}
      //       ${blockInfo.color}
      //     </div>
      //   `
      // },
      // maxX: 100,
      // splitNumber: 6,
    })
  </script>
</body>
</html>
posted @ 2024-08-23 16:47  Mizuki-Vone  阅读(64)  评论(0编辑  收藏  举报