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

本文作者:Mizuki
本文链接:https://www.cnblogs.com/mizuki-vone/p/18376398.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角【推荐】一下。您的鼓励是博主的最大动力!
本文链接:https://www.cnblogs.com/mizuki-vone/p/18376398.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角【推荐】一下。您的鼓励是博主的最大动力!
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库