20260408:归一化Y轴坐标,将每条曲线的数据按自身 min-max 归一化到 -100-100 范围

This commit is contained in:
zhugongping 2026-04-08 11:34:38 +08:00
parent 652caf9157
commit e7ae333a11

View File

@ -45,11 +45,12 @@
<el-row>
<el-col :span="24">
<el-card v-loading="loading" shadow="never">
<div slot="header">
<div slot="header" style="display: flex; align-items: center; justify-content: space-between;">
<span>{{ $t('dataCenter.history.384934-10') }}</span>
<el-switch v-model="independentYAxis" active-text="比例Y轴" inactive-text="共享Y轴" @change="handleYAxisModeChange" />
</div>
<div class="el-table--enable-row-hover el-table--medium">
<div v-show="devDatas.length !== 0" ref="devLineChart" style="width: 100%; height: 480px; background: #fff"></div>
<div v-show="devDatas.length !== 0" ref="devLineChart" :style="{ width: '100%', height: devChartHeight + 'px', background: '#fff' }"></div>
<el-empty v-if="devDatas.length === 0" style="height: 480px" :description="$t('dataCenter.history.384934-12')"></el-empty>
<el-table v-show="devTotal > 0" style="margin-top: 50px" :data="devTableList" :border="false">
<el-table-column :label="$t('dataCenter.history.384934-13')" prop="time" width="200" />
@ -125,10 +126,13 @@
</el-form-item>
</el-form>
<el-card v-loading="loading" shadow="never">
<div slot="header"><span>{{ gwResultTitle }}</span></div>
<div slot="header" style="display: flex; align-items: center; justify-content: space-between;">
<span>{{ gwResultTitle }}</span>
<el-switch v-model="independentYAxis" active-text="比例Y轴" inactive-text="共享Y轴" @change="handleYAxisModeChange" />
</div>
<!-- 计算量结果 -->
<template v-if="gwResultMode === 'calc'">
<div v-show="gwCalcDatas.length !== 0" ref="gwCalcLineChart" style="width: 100%; height: 480px"></div>
<div v-show="gwCalcDatas.length !== 0" ref="gwCalcLineChart" :style="{ width: '100%', height: gwCalcChartHeight + 'px' }"></div>
<el-empty v-if="gwCalcDatas.length === 0 && !loading" style="height: 480px" description="请选择网关、物模型(计算量)和时间范围后查询"></el-empty>
<el-table v-show="gwCalcTotal > 0" style="margin-top: 20px" :data="gwCalcTableList" border>
<el-table-column label="更新时间" prop="time" width="180" />
@ -138,7 +142,7 @@
</template>
<!-- 多设备对比结果 -->
<template v-else-if="gwResultMode === 'compare'">
<div v-show="subCompareDatas.length !== 0" ref="subCompareLineChart" style="width: 100%; height: 480px"></div>
<div v-show="subCompareDatas.length !== 0" ref="subCompareLineChart" :style="{ width: '100%', height: subCompareChartHeight + 'px' }"></div>
<el-empty v-if="subCompareDatas.length === 0 && !loading" style="height: 480px" description="请选择网关、物模型(子设备变量)和时间范围,子设备可不选(多设备对比)"></el-empty>
<el-table v-show="subCompareTotal > 0" style="margin-top: 20px" :data="subCompareTableList" border>
<el-table-column v-for="col in subCompareTableColumns" :key="col.prop" :label="col.label" :prop="col.prop" :min-width="col.prop === 'time' ? 180 : 100" />
@ -147,7 +151,7 @@
</template>
<!-- 单子设备历史结果 -->
<template v-else-if="gwResultMode === 'single'">
<div v-show="gwSingleDatas.length !== 0" ref="gwSingleLineChart" style="width: 100%; height: 480px"></div>
<div v-show="gwSingleDatas.length !== 0" ref="gwSingleLineChart" :style="{ width: '100%', height: gwSingleChartHeight + 'px' }"></div>
<el-empty v-if="gwSingleDatas.length === 0 && !loading" style="height: 480px" description="请选择网关、物模型、子设备和时间范围后查询"></el-empty>
<el-table v-show="gwSingleTotal > 0" style="margin-top: 20px" :data="gwSingleTableList" border>
<el-table-column label="更新时间" prop="time" width="180" />
@ -209,11 +213,12 @@
<el-row>
<el-col :span="24">
<el-card v-loading="loading" shadow="never">
<div slot="header">
<div slot="header" style="display: flex; align-items: center; justify-content: space-between;">
<span>{{ $t('dataCenter.history.384934-10') }}</span>
<el-switch v-model="independentYAxis" active-text="比例Y轴" inactive-text="共享Y轴" @change="handleYAxisModeChange" />
</div>
<div class="el-table--enable-row-hover el-table--medium">
<div v-show="sceneDatas.length !== 0" ref="sceneLineChart" style="width: 100%; height: 480px; background: #fff"></div>
<div v-show="sceneDatas.length !== 0" ref="sceneLineChart" :style="{ width: '100%', height: sceneChartHeight + 'px', background: '#fff' }"></div>
<el-empty v-if="sceneDatas.length === 0" style="height: 480px" :description="$t('dataCenter.history.384934-12')"></el-empty>
<el-table v-show="sceneTotal > 0" style="margin-top: 50px" :data="sceneTableList" :border="false">
<el-table-column :label="$t('dataCenter.history.384934-13')" prop="time" width="200" />
@ -378,6 +383,12 @@ export default {
gwSingleTotal: 0,
gwSinglePageNum: 1,
gwSinglePageSize: 10,
independentYAxis: true,
devChartHeight: 480,
gwCalcChartHeight: 480,
gwSingleChartHeight: 480,
subCompareChartHeight: 480,
sceneChartHeight: 480,
};
},
mounted() {
@ -801,15 +812,86 @@ export default {
}
return true;
},
calcChartHeight(seriesCount) {
return Math.min(1200, Math.max(480, seriesCount * 14 + 360));
},
applyIndependentScale(series) {
series.forEach((s) => {
const nums = (s.data || []).filter(v => v != null && v !== '').map(Number).filter(n => !isNaN(n));
if (nums.length === 0) return;
const min = Math.min(...nums);
const max = Math.max(...nums);
const range = max - min;
s.data = s.data.map(v => {
if (v == null || v === '') return null;
const num = Number(v);
if (isNaN(num)) return null;
// 映射到 -100 ~ 100,range=0 时固定为 0(居中水平线)
return {
value: range === 0 ? 0 : ((num - min) / range) * 200 - 100,
_original: v,
};
});
});
return {
type: 'value',
min: -100,
max: 100,
splitNumber: 4,
axisLine: { show: true },
axisTick: { show: true },
axisLabel: {
formatter: (val) => val + '%',
},
splitLine: {
show: true,
lineStyle: { type: 'dashed', opacity: 0.5 },
},
};
},
independentScaleTooltip() {
return {
trigger: 'axis',
formatter: (params) => {
if (!params || !params.length) return '';
let html = '<div style="font-size:12px">' + params[0].axisValueLabel + '</div>';
params.forEach((p) => {
if (p.data == null) return;
const val = (typeof p.data === 'object' && p.data._original != null)
? p.data._original : p.value;
html += p.marker + ' ' + p.seriesName + ': <b>' + val + '</b><br/>';
});
return html;
},
};
},
handleYAxisModeChange() {
this.$nextTick(() => {
if (this.activeTab === 'device') {
if (this.devDatas.length) this.drawDevLine();
} else if (this.activeTab === 'gateway') {
if (this.gwResultMode === 'calc' && this.gwCalcDatas.length) this.drawGwCalcLine();
if (this.gwResultMode === 'compare' && this.subCompareDatas.length) this.drawSubCompareLine();
if (this.gwResultMode === 'single' && this.gwSingleDatas.length) this.drawGwSingleLine();
} else if (this.activeTab === 'scene') {
if (this.sceneDatas.length) this.drawSceneLine();
}
});
},
drawDevLine() {
// 初始化折线图
const series = this.getDevSeries();
this.devChartHeight = this.calcChartHeight(series.length);
this.$nextTick(() => {
this.charts = this.$echarts.init(this.$refs.devLineChart);
this.charts.clear();
// 设置折线图数据和样式
const yAxis = this.independentYAxis
? this.applyIndependentScale(series)
: { type: 'value', scale: true };
const tooltip = this.independentYAxis
? this.independentScaleTooltip()
: { trigger: 'axis' };
this.charts.setOption({
tooltip: {
trigger: 'axis',
},
tooltip,
legend: {
align: 'right',
left: '3%',
@ -830,33 +912,19 @@ export default {
},
},
dataZoom: [
{
type: 'inside',
start: 0,
end: 100,
},
{
start: 0,
end: 100,
},
{ type: 'inside', start: 0, end: 100 },
{ start: 0, end: 100 },
],
// 自定义:设置x轴刻度
xAxis: {
type: 'category',
boundaryGap: true,
axisTick: {
alignWithLabel: true,
},
// 自定义标签
axisTick: { alignWithLabel: true },
data: this.devDatas.length !== 0 && this.devDatas.map((item) => Object.keys(item)[0]),
},
// 自定义:设置y轴刻度
yAxis: {
type: 'value',
scale: true, //自适应
},
// 设置数据
series: this.getDevSeries(),
yAxis,
series,
});
this.charts.resize();
});
},
// 获取设备的series
@ -1168,8 +1236,6 @@ export default {
},
drawGwCalcLine() {
if (!this.$refs.gwCalcLineChart) return;
const chart = this.$echarts.init(this.$refs.gwCalcLineChart);
chart.clear();
const ids = this.gwSelectedCalcIdentifiers;
const times = this.gwCalcDatas.map((d) => Object.keys(d)[0]);
const series = ids.map((id) => {
@ -1185,14 +1251,37 @@ export default {
}),
};
});
this.gwCalcChartHeight = this.calcChartHeight(series.length);
this.$nextTick(() => {
const chart = this.$echarts.init(this.$refs.gwCalcLineChart);
chart.clear();
const yAxis = this.independentYAxis
? this.applyIndependentScale(series)
: { type: 'value', scale: true };
const tooltip = this.independentYAxis
? this.independentScaleTooltip()
: { trigger: 'axis' };
chart.setOption({
tooltip: { trigger: 'axis' },
legend: { data: series.map((s) => s.name) },
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
xAxis: { type: 'category', data: times },
yAxis: { type: 'value', scale: true },
tooltip,
legend: { align: 'right', left: '3%', top: '3%' },
grid: { top: '30%', left: '5%', right: '5%', bottom: '15%', containLabel: true },
toolbox: { feature: { dataView: {}, restore: {}, saveAsImage: {} } },
dataZoom: [
{ type: 'inside', start: 0, end: 100 },
{ start: 0, end: 100 },
],
xAxis: {
type: 'category',
boundaryGap: true,
axisTick: { alignWithLabel: true },
axisLabel: { rotate: 30 },
data: times,
},
yAxis,
series,
});
chart.resize();
});
},
getGwSingleTableList(e) {
if (e) {
@ -1220,8 +1309,6 @@ export default {
},
drawGwSingleLine() {
if (!this.$refs.gwSingleLineChart || !this.gwSingleDatas.length) return;
const chart = this.$echarts.init(this.$refs.gwSingleLineChart);
chart.clear();
const ids = this.gwSelectedSubIdentifiers;
const times = this.gwSingleDatas.map((d) => Object.keys(d)[0]);
const series = ids.map((id) => {
@ -1237,14 +1324,37 @@ export default {
}),
};
});
this.gwSingleChartHeight = this.calcChartHeight(series.length);
this.$nextTick(() => {
const chart = this.$echarts.init(this.$refs.gwSingleLineChart);
chart.clear();
const yAxis = this.independentYAxis
? this.applyIndependentScale(series)
: { type: 'value', scale: true };
const tooltip = this.independentYAxis
? this.independentScaleTooltip()
: { trigger: 'axis' };
chart.setOption({
tooltip: { trigger: 'axis' },
legend: { data: series.map((s) => s.name) },
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
xAxis: { type: 'category', data: times },
yAxis: { type: 'value', scale: true },
tooltip,
legend: { align: 'right', left: '3%', top: '3%' },
grid: { top: '30%', left: '5%', right: '5%', bottom: '15%', containLabel: true },
toolbox: { feature: { dataView: {}, restore: {}, saveAsImage: {} } },
dataZoom: [
{ type: 'inside', start: 0, end: 100 },
{ start: 0, end: 100 },
],
xAxis: {
type: 'category',
boundaryGap: true,
axisTick: { alignWithLabel: true },
axisLabel: { rotate: 30 },
data: times,
},
yAxis,
series,
});
chart.resize();
});
},
getSubCompareTableList(e) {
if (e) {
@ -1282,8 +1392,6 @@ export default {
},
drawSubCompareLine() {
if (!this.$refs.subCompareLineChart || !this.subCompareDatas.length) return;
const chart = this.$echarts.init(this.$refs.subCompareLineChart);
chart.clear();
const times = this.subCompareDatas.map((d) => d.time);
const keys = Object.keys(this.subCompareDatas[0]).filter((k) => k !== 'time');
const series = keys.map((k) => ({
@ -1292,14 +1400,37 @@ export default {
smooth: true,
data: this.subCompareDatas.map((d) => d[k]),
}));
this.subCompareChartHeight = this.calcChartHeight(series.length);
this.$nextTick(() => {
const chart = this.$echarts.init(this.$refs.subCompareLineChart);
chart.clear();
const yAxis = this.independentYAxis
? this.applyIndependentScale(series)
: { type: 'value', scale: true };
const tooltip = this.independentYAxis
? this.independentScaleTooltip()
: { trigger: 'axis' };
chart.setOption({
tooltip: { trigger: 'axis' },
legend: { data: series.map((s) => s.name) },
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
xAxis: { type: 'category', data: times },
yAxis: { type: 'value', scale: true },
tooltip,
legend: { align: 'right', left: '3%', top: '3%' },
grid: { top: '30%', left: '5%', right: '5%', bottom: '15%', containLabel: true },
toolbox: { feature: { dataView: {}, restore: {}, saveAsImage: {} } },
dataZoom: [
{ type: 'inside', start: 0, end: 100 },
{ start: 0, end: 100 },
],
xAxis: {
type: 'category',
boundaryGap: true,
axisTick: { alignWithLabel: true },
axisLabel: { rotate: 30 },
data: times,
},
yAxis,
series,
});
chart.resize();
});
},
// 格式化设备列表数据
formatDevTableDatas() {
@ -1376,63 +1507,36 @@ export default {
}
},
drawSceneLine() {
// 初始化折线图
const series = this.getSceneSeries();
this.sceneChartHeight = this.calcChartHeight(series.length);
this.$nextTick(() => {
this.charts = this.$echarts.init(this.$refs.sceneLineChart);
this.charts.clear();
// 设置折线图数据和样式
const yAxis = this.independentYAxis
? this.applyIndependentScale(series)
: { type: 'value', scale: true };
const tooltip = this.independentYAxis
? this.independentScaleTooltip()
: { trigger: 'axis' };
this.charts.setOption({
tooltip: {
trigger: 'axis',
},
legend: {
align: 'right',
left: '3%',
top: '15%',
},
grid: {
top: '30%',
left: '5%',
right: '5%',
bottom: '5%',
containLabel: true,
},
toolbox: {
feature: {
dataView: {},
restore: {},
saveAsImage: {},
},
},
tooltip,
legend: { align: 'right', left: '3%', top: '15%' },
grid: { top: '30%', left: '5%', right: '5%', bottom: '5%', containLabel: true },
toolbox: { feature: { dataView: {}, restore: {}, saveAsImage: {} } },
dataZoom: [
{
type: 'inside',
start: 0,
end: 100,
},
{
start: 0,
end: 100,
},
{ type: 'inside', start: 0, end: 100 },
{ start: 0, end: 100 },
],
// 自定义:设置x轴刻度
xAxis: {
type: 'category',
boundaryGap: true,
axisTick: {
alignWithLabel: true,
},
// 自定义标签
axisTick: { alignWithLabel: true },
data: this.sceneDatas.length !== 0 && this.sceneDatas.map((item) => Object.keys(item)[0]),
},
// 自定义:设置y轴刻度
yAxis: {
type: 'value',
boundaryGap: true,
splitNumber: 4,
interval: 250,
},
// 设置数据
series: this.getSceneSeries(),
yAxis,
series,
});
this.charts.resize();
});
},
// 获取设备的series