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-row>
<el-col :span="24"> <el-col :span="24">
<el-card v-loading="loading" shadow="never"> <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> <span>{{ $t('dataCenter.history.384934-10') }}</span>
<el-switch v-model="independentYAxis" active-text="比例Y轴" inactive-text="共享Y轴" @change="handleYAxisModeChange" />
</div> </div>
<div class="el-table--enable-row-hover el-table--medium"> <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-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 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" /> <el-table-column :label="$t('dataCenter.history.384934-13')" prop="time" width="200" />
@ -125,10 +126,13 @@
</el-form-item> </el-form-item>
</el-form> </el-form>
<el-card v-loading="loading" shadow="never"> <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'"> <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-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 v-show="gwCalcTotal > 0" style="margin-top: 20px" :data="gwCalcTableList" border>
<el-table-column label="更新时间" prop="time" width="180" /> <el-table-column label="更新时间" prop="time" width="180" />
@ -138,7 +142,7 @@
</template> </template>
<!-- 多设备对比结果 --> <!-- 多设备对比结果 -->
<template v-else-if="gwResultMode === 'compare'"> <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-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 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" /> <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>
<!-- 单子设备历史结果 --> <!-- 单子设备历史结果 -->
<template v-else-if="gwResultMode === 'single'"> <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-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 v-show="gwSingleTotal > 0" style="margin-top: 20px" :data="gwSingleTableList" border>
<el-table-column label="更新时间" prop="time" width="180" /> <el-table-column label="更新时间" prop="time" width="180" />
@ -209,11 +213,12 @@
<el-row> <el-row>
<el-col :span="24"> <el-col :span="24">
<el-card v-loading="loading" shadow="never"> <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> <span>{{ $t('dataCenter.history.384934-10') }}</span>
<el-switch v-model="independentYAxis" active-text="比例Y轴" inactive-text="共享Y轴" @change="handleYAxisModeChange" />
</div> </div>
<div class="el-table--enable-row-hover el-table--medium"> <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-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 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" /> <el-table-column :label="$t('dataCenter.history.384934-13')" prop="time" width="200" />
@ -378,6 +383,12 @@ export default {
gwSingleTotal: 0, gwSingleTotal: 0,
gwSinglePageNum: 1, gwSinglePageNum: 1,
gwSinglePageSize: 10, gwSinglePageSize: 10,
independentYAxis: true,
devChartHeight: 480,
gwCalcChartHeight: 480,
gwSingleChartHeight: 480,
subCompareChartHeight: 480,
sceneChartHeight: 480,
}; };
}, },
mounted() { mounted() {
@ -801,15 +812,86 @@ export default {
} }
return true; 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() { drawDevLine() {
// 初始化折线图 const series = this.getDevSeries();
this.devChartHeight = this.calcChartHeight(series.length);
this.$nextTick(() => {
this.charts = this.$echarts.init(this.$refs.devLineChart); this.charts = this.$echarts.init(this.$refs.devLineChart);
this.charts.clear(); 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({ this.charts.setOption({
tooltip: { tooltip,
trigger: 'axis',
},
legend: { legend: {
align: 'right', align: 'right',
left: '3%', left: '3%',
@ -830,33 +912,19 @@ export default {
}, },
}, },
dataZoom: [ dataZoom: [
{ { type: 'inside', start: 0, end: 100 },
type: 'inside', { start: 0, end: 100 },
start: 0,
end: 100,
},
{
start: 0,
end: 100,
},
], ],
// 自定义:设置x轴刻度
xAxis: { xAxis: {
type: 'category', type: 'category',
boundaryGap: true, boundaryGap: true,
axisTick: { axisTick: { alignWithLabel: true },
alignWithLabel: true,
},
// 自定义标签
data: this.devDatas.length !== 0 && this.devDatas.map((item) => Object.keys(item)[0]), data: this.devDatas.length !== 0 && this.devDatas.map((item) => Object.keys(item)[0]),
}, },
// 自定义:设置y轴刻度 yAxis,
yAxis: { series,
type: 'value', });
scale: true, //自适应 this.charts.resize();
},
// 设置数据
series: this.getDevSeries(),
}); });
}, },
// 获取设备的series // 获取设备的series
@ -1168,8 +1236,6 @@ export default {
}, },
drawGwCalcLine() { drawGwCalcLine() {
if (!this.$refs.gwCalcLineChart) return; if (!this.$refs.gwCalcLineChart) return;
const chart = this.$echarts.init(this.$refs.gwCalcLineChart);
chart.clear();
const ids = this.gwSelectedCalcIdentifiers; const ids = this.gwSelectedCalcIdentifiers;
const times = this.gwCalcDatas.map((d) => Object.keys(d)[0]); const times = this.gwCalcDatas.map((d) => Object.keys(d)[0]);
const series = ids.map((id) => { 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({ chart.setOption({
tooltip: { trigger: 'axis' }, tooltip,
legend: { data: series.map((s) => s.name) }, legend: { align: 'right', left: '3%', top: '3%' },
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true }, grid: { top: '30%', left: '5%', right: '5%', bottom: '15%', containLabel: true },
xAxis: { type: 'category', data: times }, toolbox: { feature: { dataView: {}, restore: {}, saveAsImage: {} } },
yAxis: { type: 'value', scale: true }, 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, series,
}); });
chart.resize();
});
}, },
getGwSingleTableList(e) { getGwSingleTableList(e) {
if (e) { if (e) {
@ -1220,8 +1309,6 @@ export default {
}, },
drawGwSingleLine() { drawGwSingleLine() {
if (!this.$refs.gwSingleLineChart || !this.gwSingleDatas.length) return; if (!this.$refs.gwSingleLineChart || !this.gwSingleDatas.length) return;
const chart = this.$echarts.init(this.$refs.gwSingleLineChart);
chart.clear();
const ids = this.gwSelectedSubIdentifiers; const ids = this.gwSelectedSubIdentifiers;
const times = this.gwSingleDatas.map((d) => Object.keys(d)[0]); const times = this.gwSingleDatas.map((d) => Object.keys(d)[0]);
const series = ids.map((id) => { 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({ chart.setOption({
tooltip: { trigger: 'axis' }, tooltip,
legend: { data: series.map((s) => s.name) }, legend: { align: 'right', left: '3%', top: '3%' },
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true }, grid: { top: '30%', left: '5%', right: '5%', bottom: '15%', containLabel: true },
xAxis: { type: 'category', data: times }, toolbox: { feature: { dataView: {}, restore: {}, saveAsImage: {} } },
yAxis: { type: 'value', scale: true }, 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, series,
}); });
chart.resize();
});
}, },
getSubCompareTableList(e) { getSubCompareTableList(e) {
if (e) { if (e) {
@ -1282,8 +1392,6 @@ export default {
}, },
drawSubCompareLine() { drawSubCompareLine() {
if (!this.$refs.subCompareLineChart || !this.subCompareDatas.length) return; 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 times = this.subCompareDatas.map((d) => d.time);
const keys = Object.keys(this.subCompareDatas[0]).filter((k) => k !== 'time'); const keys = Object.keys(this.subCompareDatas[0]).filter((k) => k !== 'time');
const series = keys.map((k) => ({ const series = keys.map((k) => ({
@ -1292,14 +1400,37 @@ export default {
smooth: true, smooth: true,
data: this.subCompareDatas.map((d) => d[k]), 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({ chart.setOption({
tooltip: { trigger: 'axis' }, tooltip,
legend: { data: series.map((s) => s.name) }, legend: { align: 'right', left: '3%', top: '3%' },
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true }, grid: { top: '30%', left: '5%', right: '5%', bottom: '15%', containLabel: true },
xAxis: { type: 'category', data: times }, toolbox: { feature: { dataView: {}, restore: {}, saveAsImage: {} } },
yAxis: { type: 'value', scale: true }, 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, series,
}); });
chart.resize();
});
}, },
// 格式化设备列表数据 // 格式化设备列表数据
formatDevTableDatas() { formatDevTableDatas() {
@ -1376,63 +1507,36 @@ export default {
} }
}, },
drawSceneLine() { drawSceneLine() {
// 初始化折线图 const series = this.getSceneSeries();
this.sceneChartHeight = this.calcChartHeight(series.length);
this.$nextTick(() => {
this.charts = this.$echarts.init(this.$refs.sceneLineChart); this.charts = this.$echarts.init(this.$refs.sceneLineChart);
this.charts.clear(); 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({ this.charts.setOption({
tooltip: { tooltip,
trigger: 'axis', legend: { align: 'right', left: '3%', top: '15%' },
}, grid: { top: '30%', left: '5%', right: '5%', bottom: '5%', containLabel: true },
legend: { toolbox: { feature: { dataView: {}, restore: {}, saveAsImage: {} } },
align: 'right',
left: '3%',
top: '15%',
},
grid: {
top: '30%',
left: '5%',
right: '5%',
bottom: '5%',
containLabel: true,
},
toolbox: {
feature: {
dataView: {},
restore: {},
saveAsImage: {},
},
},
dataZoom: [ dataZoom: [
{ { type: 'inside', start: 0, end: 100 },
type: 'inside', { start: 0, end: 100 },
start: 0,
end: 100,
},
{
start: 0,
end: 100,
},
], ],
// 自定义:设置x轴刻度
xAxis: { xAxis: {
type: 'category', type: 'category',
boundaryGap: true, boundaryGap: true,
axisTick: { axisTick: { alignWithLabel: true },
alignWithLabel: true,
},
// 自定义标签
data: this.sceneDatas.length !== 0 && this.sceneDatas.map((item) => Object.keys(item)[0]), data: this.sceneDatas.length !== 0 && this.sceneDatas.map((item) => Object.keys(item)[0]),
}, },
// 自定义:设置y轴刻度 yAxis,
yAxis: { series,
type: 'value', });
boundaryGap: true, this.charts.resize();
splitNumber: 4,
interval: 250,
},
// 设置数据
series: this.getSceneSeries(),
}); });
}, },
// 获取设备的series // 获取设备的series