- 框架: vue2
- 通信: websocket
- 数据格式:
成品
// 瀑布图封装
<template>
<div ref="Heatmap" class="chart" />
</template>
<script>
import { chartColor } from '@/views/common/js/colorDefine'
export default {
name: 'Heatmap',
props: {
option: {
type: Object,
default: () => { }
}
},
data() {
return {
config: { // 值域等配置项
y_domain: [-40, 100], // y轴值域 [min,max]
y_Initialize: [-20, 80], // y轴初始化显示值域 [startValue, endValue]
},
grid: { left: 30, right: 25, top: 25, bottom: 18, width: 0, height: 0 },
xAxis: { name: 'MHz' },
chartTimer: null, // 延时器
renderData: { xAxis: [], yAxis: [], series: {}}, // 数据存储
mousePosition: { x: '', y: '' }, // 鼠标位置存储
elementBlock: { b_w: '', b_h: '' }, // 元素块宽高
eventType: '', // 事件类型
chartName: '', // chart图名称
canRender: true,
}
},
mounted() {
this.creatChart()
},
activated() {
if (this.getChart()) {
this.resize()
this.echartsResize()
}
this.canRender = true
},
deactivated() {
this.getChart() && this.resize('remove')
this.canRender = false
},
beforeDestroy() {
this.getChart() && this.resize('remove')
this.canRender = false
},
methods: {
// 创建chart 并插入dom
creatChart() {
var obj = this.option
if (obj.config) this.config = Object.assign(this.config, obj.config)
if (obj.grid) this.grid = Object.assign(this.grid, obj.grid)
if (obj.xAxis) this.xAxis = Object.assign(this.xAxis, obj.xAxis)
if (obj.chartName) this.chartName = obj.chartName
var myCanvas = document.createElement('canvas')
this.getDom().appendChild(myCanvas)
this.initChart()
// 添加tooltip
this.createTooltip()
// 窗口监听
this.resize()
// 添加监听
this.addMonitor()
this.$emit('chartFun', this.$CONST_MI_COMMAND.MI_echarts_created, this)
},
// 创建chart 仅渲染grid框
initChart() {
var myCanvas = this.getChart()
var ctx = myCanvas.getContext('2d')
var canvasBox = this.getDom()
var s = canvasBox.getBoundingClientRect()
this.grid.width = parseInt(s.width) - 5
this.grid.height = parseInt(s.height) - 6
const { left, right, top, bottom, width, height } = this.grid
myCanvas.width = width
myCanvas.height = height
ctx.fillStyle = chartColor.COLOR_BACK_GROUND
ctx.fillRect(0, 0, width, height)
ctx.clearRect(0, 0, width, height)
// 创建grid
ctx.beginPath()
ctx.strokeStyle = chartColor.COLOR_GRID
ctx.moveTo(left, top)
ctx.lineTo(width - right, top)
ctx.lineTo(width - right, height - bottom)
ctx.lineTo(left, height - bottom)
ctx.lineTo(left, top)
ctx.closePath()
ctx.stroke()
this.createVisualMap()
this.addChartAttr()
// 清空数据
this.renderData = { xAxis: [], yAxis: [], series: {}}
// this.syncShowX();
this.renderChart(JSON.parse(JSON.stringify(this.renderData)))
},
// 创建视觉映射组件
createVisualMap() {
const min = this.config.y_Initialize[0]
const max = this.config.y_Initialize[1]
var myCanvas = this.getChart()
var ctx = myCanvas.getContext('2d')
const { left, right, top, bottom, width, height } = this.grid
ctx.clearRect(width - right + 7, top, 18, height - bottom - top)
var grd = ctx.createLinearGradient(width - right + 7, top, width - right + 7, height - bottom)
grd.addColorStop(0, this.blockColor(max))
grd.addColorStop(0.5, this.blockColor((max + min) / 2))
grd.addColorStop(1, this.blockColor(min))
ctx.fillStyle = grd
ctx.fillRect(width - right + 7, top, 18, height - bottom - top)
ctx.font = '9px sans-serif'
ctx.fillStyle = chartColor.COLOR_CURSOR_TEXT
ctx.fillText(min, computedLeft(min), height - bottom - 3)
ctx.fillText(max, computedLeft(max), top + 10)
function computedLeft(val) {
const a = String(val).length
return width - right + (a === 3 ? 8 : a === 2 ? 10 : 14)
}
},
// 根据配置初始化其他属性
addChartAttr() {
var ctx = this.getChart().getContext('2d')
const { left, right, top, bottom, width, height } = this.grid
// 上箭头
ctx.beginPath()
ctx.strokeStyle = chartColor.COLOR_CURSOR_TEXT
ctx.moveTo(width - right + 16, top - 4)
ctx.lineTo(width - right + 16, top - 16)
ctx.lineTo(width - right + 10, top - 10)
ctx.lineTo(width - right + 16, top - 20)
ctx.lineTo(width - right + 22, top - 10)
ctx.lineTo(width - right + 16, top - 16)
ctx.closePath()
ctx.stroke()
ctx.fill()
// 下箭头
ctx.beginPath()
ctx.moveTo(width - right + 16, height - bottom + 4)
ctx.lineTo(width - right + 16, height - bottom + 16)
ctx.lineTo(width - right + 10, height - bottom + 10)
ctx.lineTo(width - right + 16, height - bottom + 20)
ctx.lineTo(width - right + 22, height - bottom + 10)
ctx.lineTo(width - right + 16, height - bottom + 16)
ctx.closePath()
ctx.stroke()
ctx.fill()
// 放大
ctx.beginPath()
ctx.arc(width - right - 34, top - 14, 6, 0, 2 * Math.PI)
ctx.moveTo(width - right - 37, top - 14)
ctx.lineTo(width - right - 31, top - 14)
ctx.moveTo(width - right - 34, top - 11)
ctx.lineTo(width - right - 34, top - 17)
ctx.moveTo(width - right - 29, top - 9)
ctx.lineTo(width - right - 25, top - 4)
ctx.closePath()
ctx.stroke()
// 缩小
ctx.beginPath()
ctx.arc(width - right - 10, top - 14, 6, 0, 2 * Math.PI)
ctx.moveTo(width - right - 13, top - 14)
ctx.lineTo(width - right - 7, top - 14)
ctx.moveTo(width - right - 5, top - 9)
ctx.lineTo(width - right - 1, top - 4)
ctx.closePath()
ctx.stroke()
// chart图名称
ctx.font = '12px sans-serif'
ctx.fillStyle = chartColor.COLOR_CHARTNAME
ctx.fillText(this.chartName, width * 0.484, height)
},
// 渲染xy轴
renderXY(type) {
const { xAxis, yAxis, min, max } = this.renderData
if (!xAxis.length || !xAxis.length) return
var x1 = xAxis[0]
var x2 = xAxis[xAxis.length - 1]
var y1 = yAxis[0]
var y2 = yAxis[99]
const { left, right, top, bottom, width, height } = this.grid
// 计算每一小块的宽高、包含边界 1px => + 1
const b_w = (width - left - right) / xAxis.length
const b_h = (height - top - bottom) / 100
this.elementBlock = { b_w, b_h }
var ctx = this.getChart().getContext('2d')
if (y1) y1 = y1.split(':')[2]
if (y2) y2 = y2.split(':')[2]
// 清除原文本
ctx.clearRect(0, top - 3, left, height - top - bottom + 10)
ctx.clearRect(left, height - bottom + 5, width - left - right, 18)
ctx.font = '9px sans-serif'
ctx.fillStyle = chartColor.COLOR_SCALE_TEXT
ctx.fillText(min || '', left, height - bottom + 14)
ctx.fillText(max || '', width - right - String(max).length * 5, height - bottom + 14)
ctx.fillText(y1 || '', 2, top + 8)
ctx.fillText(y2 || '', 2, height - bottom + 2)
// 保存空数据时canvas状态
ctx.save()
},
// 渲染数据
renderChart(data) {
if (!this.canRender) return
data.series = Object.values(data.series).reverse()
this.renderData = data
var myCanvas = this.getChart()
if (!myCanvas) return
var ctx = myCanvas.getContext('2d')
// 返回渲染前状态 => 清除上一次渲染
ctx.restore()
this.renderXY()
const { left, top } = this.grid
const { b_w, b_h } = this.elementBlock
this.renderData.series.map((item, index) => {
item.map(($item, $index) => {
const x = $index * b_w + left
const y = index * b_h + top
if ($item != null) {
ctx.fillStyle = this.blockColor($item)
ctx.fillRect(x, y, b_w, b_h)
}
})
})
this.showTooltip()
},
// 添加 tooltip
createTooltip() {
const { left, right, top, bottom, width, height } = this.grid
var dom = this.getDom()
// hLine vLine 横竖线条
var hLine = dom.querySelector('.hLine')
var vLine = dom.querySelector('.vLine')
var tooltip = dom.querySelector('.tooltip')
if (!hLine) hLine = document.createElement('div')
if (!vLine) vLine = document.createElement('div')
if (!tooltip) tooltip = document.createElement('div')
hLine.className = 'hLine'
vLine.className = 'vLine'
tooltip.className = 'tooltip'
const commonStyle = {
position: 'absolute',
display: 'none',
'z-index': '30',
'pointer-events': 'none',
}
const hLineStyle = {
...commonStyle,
background: chartColor.COLOR_TOOLTIP_LINE,
width: width - left - right + 'px',
height: '1px',
left: left + 'px',
top: 0
}
const vLineStyle = {
...commonStyle,
background: chartColor.COLOR_TOOLTIP_LINE,
width: '1px',
height: height - top - bottom + 'px',
left: 0,
top: top + 'px'
}
const toolbipStyle = {
...commonStyle,
transition: 'left 0.4s cubic-bezier(0.23, 1, 0.32, 1) 0s, top 0.4s cubic-bezier(0.23, 1, 0.32, 1) 0s',
'background-color': 'rgba(50, 50, 50, 0.7)',
'border-radius': '4px',
color: chartColor.COLOR_CURSOR_TEXT,
font: '14px / 21px "Microsoft YaHei"',
padding: '5px',
left: '100px',
top: '100px',
'min-width': '120px',
'min-height': '50px',
}
for (const key in hLineStyle) {
hLine.style[key] = hLineStyle[key]
}
for (const key in vLineStyle) {
vLine.style[key] = vLineStyle[key]
}
for (const key in toolbipStyle) {
tooltip.style[key] = toolbipStyle[key]
}
tooltip.innerHTML = `
<div class="y_val"></div>
<div class="x_val"></div>
<div class="chart_val"</div>
`
dom.appendChild(hLine)
dom.appendChild(vLine)
dom.appendChild(tooltip)
},
// 窗口大小更改 重新计算vline hline tooltip位置
computeTooltip() {
const { left, right, top, bottom, width, height } = this.grid
var dom = this.getDom()
var hLine = dom.querySelector('.hLine')
var vLine = dom.querySelector('.vLine')
hLine.style.width = width - left - right + 'px'
vLine.style.height = height - top - bottom + 'px'
},
// 添加监听
addMonitor() {
var dom = this.getDom()
dom.onmouseenter = () => {
dom.onmousemove = this.domMousemoveEvent
dom.onclick = (e) => {
if (this.eventType) this.changeVisualMap()
}
dom.onmouseleave = () => {
dom.onmousemove = ''
dom.onmouseleave = ''
dom.onclick = ''
this.mousePosition = { x: '', y: '' }
}
}
},
domMousemoveEvent(e) {
var dom = this.getDom()
this.regionEvent(e)
dom.style.cursor = this.eventType ? 'pointer' : 'default'
if (!this.renderData.xAxis.length) return
const x = e.offsetX
const y = e.offsetY
this.mousePosition = { x, y }
// 超出边界不计算
var hLine = dom.querySelector('.hLine')
var vLine = dom.querySelector('.vLine')
var position = this.computeMousePosition()
this.$emit('chartFun', this.$CONST_MI_COMMAND.MI_echarts_recordLeft, position)
if (!this.computeMousePosition()) {
hLine.style.display = 'none'
vLine.style.display = 'none'
this.showTooltip()
return
}
hLine.style.display = 'block'
hLine.style.top = y + 'px'
vLine.style.display = 'block'
vLine.style.left = x + 'px'
this.showTooltip()
},
// 计算滑动区域是否属于事件区域
regionEvent(e) {
const x = e.offsetX
const y = e.offsetY
const { left, right, top, bottom, width, height } = this.grid
// up: 上箭头 down:下箭头 enlarge:放大 narrow:缩小 wh:点击区域大小
const wh = 18
const region = {
up: { l: width - right + 7, r: width - right + 7 + wh, t: top - 22, b: top - 22 + wh },
down: { l: width - right + 7, r: width - right + 7 + wh, t: height - bottom + 4, b: height - bottom + 4 + wh },
enlarge: { l: width - right - wh - wh - 6, r: width - right - wh - 6, t: top - 22, b: top - 22 + wh },
narrow: { l: width - right - wh, r: width - right, t: top - 22, b: top - 22 + wh },
}
let eventType = ''
for (const key in region) {
const { l, r, t, b } = region[key]
if (x >= l && x <= r && y >= t && y <= b) eventType = key
}
this.eventType = eventType
},
// 操作视觉映射组件
changeVisualMap() {
const min = this.config.y_domain[0]
const max = this.config.y_domain[1]
let show_min = this.config.y_Initialize[0]
let show_max = this.config.y_Initialize[1]
const show_length = show_max - show_min
switch (this.eventType) {
case 'up':
show_max += 10
if (show_max > max) show_max = max
show_min = show_max - show_length
break
case 'down':
show_min -= 10
if (show_min < min) show_min = min
show_max = show_min + show_length
break
case 'enlarge':
if (show_length > 10) {
show_max -= 10
show_min += 10
if (show_min >= show_max) {
show_max = show_min
show_min = show_max - 10
}
}
break
case 'narrow':
show_max += 10
show_min -= 10
if (show_max > max) show_max = max
if (show_min < min) show_min = min
break
}
if (show_min !== this.config.y_Initialize[0] || show_max !== this.config.y_Initialize[1]) {
this.$set(this.config, 'y_Initialize', [show_min, show_max])
this.createVisualMap()
this.$nextTick(() => {
this.$emit('chartFun', this.$CONST_MI_COMMAND.MI_fall_computeVisual)
})
}
},
// 实时显示tooltip内容
showTooltip() {
var tooltip = this.getDom().querySelector('.tooltip')
if (!tooltip) return
const { width, height } = this.grid
const { x, y } = this.mousePosition
var position = this.computeMousePosition()
if (!position) return tooltip.style.display = 'none'
const { x_index, y_index } = position
var y_dom = tooltip.querySelector('.y_val')
var x_dom = tooltip.querySelector('.x_val')
var chart_dom = tooltip.querySelector('.chart_val')
const x_val = this.renderData.xAxis[x_index]
const y_val = this.renderData.yAxis[y_index]
var chart_val = ''
if (this.renderData.series[y_index]) chart_val = this.renderData.series[y_index][x_index]
tooltip.style.display = 'block'
tooltip.style.top = ((y + 70) > height ? (y - 70) : y) + 'px'
tooltip.style.left = ((x + 140) > width ? (x - 140) : (x + 20)) + 'px'
y_dom.innerText = `时间:${y_val || ''}`
x_dom.innerText = `频率:${x_val}MHz`
chart_dom.innerText = `电平:${chart_val || ''}`
},
// 同步显示tooltip
syncShowX(e) {
var dom = this.getDom()
var vLine = dom.querySelector('.vLine')
var hLine = dom.querySelector('.hLine')
var tooltip = dom.querySelector('.tooltip')
if (!vLine || !hLine || !tooltip) return
hLine.style.display = 'none'
tooltip.style.display = 'none'
if (!this.renderData.xAxis.length || !e.msgdata) return vLine.style.display = 'none'
const x = this.renderData.xAxis.findIndex(item => item === e.msgdata.x)
if (x === -1) return
vLine.style.display = 'block'
vLine.style.left = this.elementBlock.b_w * (x + 0.5) + this.grid.left + 'px'
},
// 计算鼠标位置
computeMousePosition() {
const { xAxis, yAxis } = this.renderData
if (!xAxis.length || !xAxis.length) return false
const { x, y } = this.mousePosition
const { b_w, b_h } = this.elementBlock
const { left, right, top, bottom, width, height } = this.grid
if (!x || !y || x < left || x >= width - right || y < top || y > height - bottom) return false
const x_index = Math.floor((x - left) / b_w)
const y_index = Math.floor((y - top) / b_h)
const x_val = xAxis[x_index]
const y_val = xAxis[y_index]
return { x_index, y_index, x_val, y_val }
},
// 视窗监听
resize(type) {
type ? window.removeEventListener('resize', this.echartsResize) : window.addEventListener('resize', this.echartsResize)
},
echartsResize() {
if (this.getChart()) {
if (this.chartTimer) {
clearTimeout(this.chartTimer)
this.chartTimer = ''
}
this.chartTimer = setTimeout(() => {
this.initChart()
this.renderXY()
this.computeTooltip()
this.chartTimer = ''
}, 300)
}
},
// 根据val转化为color
blockColor(val) {
const c_min = parseInt(chartColor.COLOR_VISUAL_START.replace('#', ''), 16)
const c_mid = parseInt(chartColor.COLOR_VISUAL_MIDDLE.replace('#', ''), 16)
const c_max = parseInt(chartColor.COLOR_VISUAL_END.replace('#', ''), 16)
const min = this.config.y_Initialize[0]
const mid = (this.config.y_Initialize[1] + this.config.y_Initialize[0]) / 2
const max = this.config.y_Initialize[1]
var color = ''
if (val > max || val < min) return chartColor.COLOR_BACK_GROUND
if (val >= mid) {
color = parseInt(((val - mid) / (max - mid)) * (c_max - c_mid) + c_mid).toString(16)
} else {
color = parseInt(((val - min) / (mid - min)) * (mid - c_min) + c_min).toString(16)
}
let str = '#'
for (let i = 0; i < 6 - color.length; i++) {
str += '0'
}
str += color
return str
},
// 获取echart实例
getChart() {
const dom = this.getDom()
if (!dom) return ''
return dom.querySelector('canvas')
},
// 获取dom
getDom() {
return this.$refs.Heatmap
}
},
}
</script>
<style>
</style>
瀑布图调用
<Heatmap v-if="startRendering" :option="option" @chartFun="chartFun" />
data() {
return {
myChart:'',
option: {
chartName: '',
},
chartData: {
xAxis: [],
yAxis: [],
series: {},
min: '',
max: ''
},
factor: null,// 比例因子 页面大小变化的时候更改 用于重绘瀑布图
}
},
methods: {
// 处理soket来的数据
setChartData(data){
if (!this.myChart) return
if (!this.factor || this.factor !== data.msgdata.factor) this.createCoordsX(data.msgdata.factor)
this.chartData.series[data.time] = data.data
// 取100帧数据 series中最新100条数据进行渲染即可,其他的删除 然后
// 优化: 可将下面部分放置再新的方法中, 使用定时器进行渲染
// 调用 this.myChart.renderChart(JSON.parse(JSON.stringify(this.chartData)))
const keys = Object.keys(this.chartData.series)
if (keys.length > 100) {
const surplusKeys = keys.slice(0, -100)
surplusKeys.map(key => {
delete this.chartData.series[key]
})
}
this.chartData.yAxis = this.chartData.yAxis.slice(-100)
this.myChart.renderChart(JSON.parse(JSON.stringify(this.chartData)))
},
// 创建坐标集
createCoords(){
var xAxis = Array
var min = number / string
var max = number / string
this.$set(this.chartData, 'xAxis', xAxis)
this.$set(this.chartData, 'yAxis', [])
this.$set(this.chartData, 'min', min)
this.$set(this.chartData, 'max', max)
},
chartFun(type, data) {
switch (type) {
case this.$CONST_MI_COMMAND.MI_echarts_created:
this.myChart = data
break
case this.$CONST_MI_COMMAND.MI_fall_computeVisual:
xxx
break
case this.$CONST_MI_COMMAND.MI_echarts_recordLeft:
xxxx
break
}
},
}