Echarts - 多图表联动范围选择 且 选择器固定顶部实现
效果图
功能点
- 多个图表仅靠最顶层的范围选择即可一起联动
- 范围选择固定在顶部,不随图表一起滚动
- 图表动态生成 高度动态 标题自定义动态 位置动态
- 异常区间告警(方法注释了,需要可以开启)
实现
这里不过多解释了,直接贴代码,有需要的可以用心阅读下
ps:顶部固定是采用双图表实现的
// 动态生成的方法
// 动态生成图表初始化配置
export function getInitChartOption(data: any[], dateList: string[]) {
const formatter = (params: any) => {
let result = params[0].name + '<br>' // 显示时间
params.forEach((item: any) => {
const unit = data.find((d) => d.name === item.seriesName).unit || ''
result += `${item.marker} ${item.seriesName}: ${item.value} ${unit}<br>`
})
return result
}
const dataZoomObj = {
xAxisIndex1: [0, data.length - 1],
xAxisIndex2: data.map((_item, index) => index)
}
const grid = data.map((_i, index) => {
return {
left: 40,
right: 40,
// top: 275 * index + 75 + '',
top: 275 * index + 60 + '',
height: '200'
}
})
const xAxis = data.map((_i, index) => {
return {
gridIndex: index,
type: 'category',
boundaryGap: false,
axisLine: {
onZero: true,
lineStyle: {
color: '#fff'
}
},
data: dateList,
axisLabel: {
color: '#fff'
}
}
})
const yAxis = data.map((item, index) => {
return {
gridIndex: index,
// name: item.name + '(' + item.unit + ')',
type: 'value',
axisLabel: {
color: '#fff'
},
axisLine: {
lineStyle: {
color: '#fff'
}
},
splitLine: {
show: true,
lineStyle: {
color: '#107779',
type: 'dashed'
}
}
// min: item.name === '二氧化碳' ? 25 : undefined
}
})
const series = data.map((item, index) => {
return {
name: item.name,
type: 'line',
// lineStyle: {
// color: '#00DAA0'
// },
markArea: {
show: item.markAreaData?.length,
itemStyle: {
color: 'rgba(255,0,0,0.15)'
},
data: item.markAreaData
},
xAxisIndex: index,
yAxisIndex: index,
symbolSize: 8,
data: item.data
// itemStyle: {
// color: '#00DAA0'
// }
}
})
const option = {
title: {
text: '',
left: 'center'
},
tooltip: {
trigger: 'axis',
axisPointer: {
animation: false
},
formatter
},
axisPointer: {
link: [
{
xAxisIndex: 'all'
}
]
},
dataZoom: [
{
show: true,
realtime: true,
start: 80,
end: 100,
xAxisIndex: dataZoomObj.xAxisIndex1,
top: '100%',
// bottom: '0%',
left: '12%',
right: '12%',
textStyle: {
// 新增/修改文本样式配置
color: '#FFFFFF', // 白色文字
fontSize: 12
}
// orient: 'vertical',
},
{
type: 'inside',
realtime: true,
orient: 'vertical',
start: 80,
end: 100,
xAxisIndex: dataZoomObj.xAxisIndex2,
zoomOnMouseWheel: 'alt'
}
],
grid,
xAxis,
yAxis,
series
// visualMap: {
// show: false,
// dimension: 0,
// pieces: [
// {
// gt: 2,
// lte: 6,
// color: 'red'
// },
// {
// gt: 8,
// lte: 10,
// color: 'yellow'
// },
// ]
// }
}
return option
}
// 动态生成滑块图表初始化配置
export function getInitChartSliderOption(data: any[], dateList: string[]) {
const dataZoomObj = {
xAxisIndex1: [0, data.length - 1],
xAxisIndex2: data.map((_item, index) => index)
}
const option = {
title: {
text: '',
left: 'center'
},
tooltip: {
show: false
},
dataZoom: [
{
show: true,
realtime: true,
start: 80,
end: 100,
xAxisIndex: dataZoomObj.xAxisIndex1,
top: '0%',
left: '12%',
right: '12%',
textStyle: {
// 新增/修改文本样式配置
color: '#FFFFFF', // 白色文字
fontSize: 12
}
// orient: 'vertical',
},
{
type: 'inside',
realtime: true,
orient: 'vertical',
start: 80,
end: 100,
xAxisIndex: dataZoomObj.xAxisIndex2,
zoomOnMouseWheel: 'alt'
}
],
grid: {
left: '12%',
right: '12%',
top: '20%',
bottom: '20%'
},
xAxis: {
show: false,
type: 'category',
data: dateList,
position: 'bottom'
},
yAxis: {
show: false
},
series: [
{
type: 'line',
data: Array(dateList.length).fill(0),
showSymbol: false,
lineStyle: { opacity: 0 }
}
]
}
return option
}
// 获取异常区间数据
export const getWarningFn = (data: any[], basicData: any[]) => {
const warnObj: any = {},
middleObj: any = {}
data.forEach((item, index) => {
basicData.forEach((iten) => {
if (!warnObj[iten.key] && iten.basic) {
warnObj[iten.key] = []
middleObj[iten.key] = []
}
if (judegNext(data, item, iten, index)) {
const arr = middleObj[iten.key]
if (arr.length === 0) {
arr.push({ xAxis: item.uploadTime }) // 记录开始时间
}
} else if (iten.basic) {
const arr = middleObj[iten.key]
if (arr.length > 0) {
arr.push({ xAxis: item.uploadTime }) // 记录结束时间
warnObj[iten.key].push([...arr])
middleObj[iten.key] = []
}
}
})
})
console.log(warnObj, 'warnObj----------------------->')
return warnObj
}
function judegNext(data: any, item: any, iten: any, index: number) {
const len = data.length
if (index + 1 > len) return false
const nextItem = data[index + 1]
console.log(nextItem, 'nextItem----------------------->')
if (
iten.basic &&
item[iten.key] &&
Number(item[iten.key]) > iten.basic &&
nextItem?.[iten.key] &&
nextItem[iten.key] > iten.basic
)
return true
return false
}
文件代码
<template>
<div class="device-echarts">
<el-empty
description="暂无数据"
v-if="noData || loading"
v-loading="loading"
element-loading-background="#094a5b"
/>
<div class="echart-box" v-else>
<v-chart
v-show="!noData && !loading"
class="echart-slider"
ref="mapChartSlider"
:option="rankChart"
autoresize
/>
<div class="scroll-box">
<div class="echart-wrapper">
<v-chart
v-show="!noData && !loading"
class="echart"
ref="mapChart"
:option="rankChart"
autoresize
/>
<template v-if="titleList.length > 0 && !loading && !noData">
<div
class="title-item"
v-for="item in titleList"
:key="item.key"
:style="{ top: item.top }"
>
<img :src="titlePre" alt="" />
<span>{{ item.name }}</span>
</div>
</template>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { nextTick, onMounted, ref } from 'vue'
import {
getData,
getRandomNum,
getInitChartOption,
getInitChartSliderOption,
testData,
waterQualityData,
electricityData,
rubbishData,
getKeyValue,
toiletDeviceData,
getWarningFn,
handleOverflow
} from './echart'
import {
queryWaterDeviceHistoryData,
queryToiletDeviceHistoryData,
queryElecDeviceHistoryData,
queryRubbishDeviceHistoryData
} from '@/api/manageDispatch/device'
import titlePre from '@/assets/images/manageDispatch/device/title-pre.png'
import dayjs from 'dayjs'
import * as echarts from 'echarts'
const props = defineProps({
data: {
type: Object
},
detailType: {
type: String
}
})
const rankChart = ref<any>(null)
const rankChartSlider = ref<any>(null)
const mapChart = ref<any>(null)
const mapChartSlider = ref<any>(null)
// 100 * 1600 / 1080) vh 400 * 4(个数)
const chartHeight = ref('0vh') // 400 * 4(个数)
const loading = ref(false)
const noData = ref(true)
const titleList = ref<any>([])
function getHistoryData() {
loading.value = true
// 以前一天为基准 获取一个月的范围时间
const latsDay = dayjs().subtract(1, 'day').format('YYYY-MM-DD') + ' 23:59:59'
const latsMonthDay = dayjs().subtract(1, 'month').format('YYYY-MM-DD') + ' 00:00:00'
// 不同设备类型 获取不同的数据
let len = 0,
arr = [],
fn = queryWaterDeviceHistoryData
if (props.detailType === 'toilet') {
len = toiletDeviceData.value.length
arr = toiletDeviceData.value
fn = queryToiletDeviceHistoryData
} else if (props.detailType === 'electricity') {
len = electricityData.value.length
arr = electricityData.value
fn = queryElecDeviceHistoryData
} else if (props.detailType === 'rubbish') {
len = rubbishData.value.length
arr = rubbishData.value
fn = queryRubbishDeviceHistoryData
} else {
len = waterQualityData.value.length
arr = waterQualityData.value
}
const target = getKeyValue(arr)
titleList.value = arr.map((item: any, index: number) => {
// const top = index * 375 + 70
// const he = (100 * top) / 1080 + 'vh'
const top = index * 275 + 18
const newHe = top + 'px'
return {
name: item.unit ? item.name + '(' + item.unit + ')' : item.name,
top: newHe,
key: item.key
}
})
const dateList: string[] = []
fn({
deviceId: props.data?.deviceId,
startTime: latsMonthDay,
endTime: latsDay
})
.then((res) => {
let arrData = res
if (!res?.length) {
noData.value = true
return
}
if (props.detailType === 'toilet') {
arrData = res?.[0].toiletAlarmRecordData
}
if (!arrData?.length) {
noData.value = true
return
}
// 获取报警区间 面积数据
// const AreaTarget: any = getWarningFn(res, arr)
getChartHeight(len)
noData.value = false
arrData.forEach((item: any) => {
const time = dayjs(item.uploadTime || item.time).format('MM-DD HH:mm')
dateList.push(time)
for (const key in target) {
if (item[key]) {
target[key].push(item[key])
} else {
target[key].push(0)
}
}
})
// console.log(target, 'target---------------->')
// console.log(dateList, 'dateList---------------->')
arr.forEach((item: any) => {
item.data = target[item.key]
// item.markAreaData = AreaTarget[item.key]?.length > 0 ? AreaTarget[item.key] : []
})
initChart(arr, dateList)
})
.finally(() => {
loading.value = false
})
}
function getChartHeight(num: number) {
const h = num * 400
const he = (100 * h) / 1080 + 'vh'
const newHeight = 270 * num + 100 + 'px'
chartHeight.value = newHeight
}
getChartHeight(1)
onMounted(() => {
getHistoryData() // 真实后台请求数据
// textEchat() // 测试数据
})
function initChart(data: any, dateList: any) {
const option = getInitChartOption(data, dateList)
// 使用刚指定的配置项和数据显示图表。
rankChart.value = option
const optionSlider = getInitChartSliderOption(data, dateList)
rankChartSlider.value = optionSlider
setTimeout(() => {
nextTick(() => {
const mainChart = mapChart.value?.chart
mainChart.setOption({
dataZoom: [
{
start: 80,
end: 100,
top: '100%'
},
{
start: 80,
end: 100
}
]
})
const sliderChart = mapChartSlider.value?.chart
sliderChart.setOption({
dataZoom: [
{
start: 80,
end: 100,
top: '0%'
},
{
start: 80,
end: 100
}
]
})
console.log(mainChart, 'mainChart---------------->', sliderChart)
// 监听 control 图表的 datazoom 事件,并同步到 main 图表
// echarts.connect([sliderChart, mainChart])
syncDataZoom(sliderChart, mainChart)
})
}, 100)
// setTimeout(() => {
// nextTick(() => {
// handleOverflow(mapChart.value.chart, option)
// })
// }, 1000)
// handleOverflow(mapChart.value)
}
function textEchat() {
noData.value = false
loading.value = false
titleList.value = testData.value.map((item: any, index: number) => {
const top = index * 275 + 18
const he = (100 * top) / 1080 + 'vh'
const newHe = top + 'px'
return {
name: item.unit ? item.name + '(' + item.unit + ')' : item.name,
top: newHe,
key: item.key
}
})
getChartHeight(testData.value.length)
const data = getData()
const dateList = data.map((str) => str.replace('2025/', ''))
testData.value[0].data = data.map(() => getRandomNum(1, 80) / 100)
testData.value[1].data = data.map(() => getRandomNum(1, 90) / 100)
testData.value[2].data = data.map(() => getRandomNum(1, 100))
testData.value[3].data = data.map(() => getRandomNum(1, 45))
testData.value[4].data = data.map(() => getRandomNum(30, 95))
const option = getInitChartOption(testData.value, dateList)
// 使用刚指定的配置项和数据显示图表。
rankChart.value = option
const optionSlider = getInitChartSliderOption(testData.value, dateList)
rankChartSlider.value = optionSlider
setTimeout(() => {
nextTick(() => {
const mainChart = mapChart.value?.chart
const sliderChart = mapChartSlider.value?.chart
console.log(mainChart, 'mainChart---------------->', sliderChart)
// 监听 control 图表的 datazoom 事件,并同步到 main 图表
// echarts.connect([sliderChart, mainChart])
syncDataZoom(sliderChart, mainChart)
mainChart.setOption({
dataZoom: [
{
start: 80,
end: 100,
top: '100%'
},
{
start: 80,
end: 100
}
]
})
})
}, 1000)
}
function syncDataZoom(fromChart: any, toChart: any) {
fromChart.on('datazoom', (params: any) => {
toChart.setOption({
dataZoom: [
{
start: params.start,
end: params.end,
top: '100%'
},
{
start: params.start,
end: params.end
}
]
})
})
}
</script>
<style scoped lang="scss">
.device-echarts {
width: 100%;
margin-top: height(20);
.scroll-box {
max-height: height(750);
// height: height(800);
overflow: auto;
// 滚动条样式
&::-webkit-scrollbar {
width: 5px;
height: 5px;
background-color: transparent;
}
&::-webkit-scrollbar-thumb {
background-color: #2e5d5f;
border-radius: 5px;
}
.echart-wrapper {
width: 100%;
height: v-bind(chartHeight);
// height: 1600px; // 调试测试数据开启
position: relative;
}
}
.echart-box {
width: 100%;
// height: v-bind(chartHeight);
// height: 1600px;
// height: height(1600);
// height: height(2000);
position: relative;
}
.echart-slider {
width: 100%;
height: 43px;
overflow: hidden;
}
.echart {
width: 100%;
height: 100%;
// height: v-bind(chartHeight);
// height: height(1600);
}
.title-item {
position: absolute;
top: height(70);
// top: height(450);
// top: height(830);
left: 50px;
display: flex;
align-items: center;
> img {
width: 27px;
height: 24px;
margin-right: 17px;
}
> span {
font-family:
Alibaba PuHuiTi,
Alibaba PuHuiTi;
font-weight: 500;
font-size: 20px;
text-align: left;
font-style: normal;
text-transform: none;
background: linear-gradient(90deg, #ffffff 0%, #e2e9ff 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
}
}
</style>
End
有问题可以留言,看见回回复的