Commit bc1dfb66 authored by 刘梦焕's avatar 刘梦焕

feat: 新增经验报警曲线

parent 65e26fda
import React from 'react';
import {EmpiricalCurve} from '../index';
import React, { useState } from 'react';
import { Button } from 'antd';
import { EmpiricalCurve } from '../index';
const Demos = () => {
const [visible, setVisible] = useState(false);
const deviceParams = {
deviceCode: 'EGBF00000120',
sensors: '出水瞬时流量',
deviceType: '二供泵房',
title: '经验报警曲线',
visible,
onClose: () => {
setVisible(false);
},
};
return (
<>
<EmpiricalCurve />
{visible && <EmpiricalCurve {...deviceParams} />}
<Button onClick={() => setVisible(true)}>打开经验报警曲线</Button>
</>
);
};
......
import React, { useContext } from 'react';
import { ConfigProvider } from 'antd';
import React, { useContext, useEffect, useRef, useState, useMemo } from 'react';
import { ConfigProvider, Modal, Radio, Select, InputNumber, Spin, message } from 'antd';
import classNames from 'classnames';
import { BasicChart } from '@wisdom-components/basicchart';
import { getStatisticsInfo } from '../apis';
import { getOptions, experiTypeOptions, compareOptions, valTakeOptions } from './utils';
import moment from 'moment';
import _ from 'lodash';
import './index.less';
const EmpiricalCurve = () => {
const FORMAT = 'yyyy-MM-DD HH:mm:ss';
const PredictionCurve = (props) => {
const { getPrefixCls } = useContext(ConfigProvider.ConfigContext);
const prefixCls = getPrefixCls('empirical-curve');
const {
width,
deviceCode,
sensors,
deviceType,
getContainer,
title,
visible,
onClose,
valTakeType = '按偏移量',
compareType = '同比',
experiType = 'h',
highLimit = 2,
higherLimit = 0,
} = props;
const [loading, setLoading] = useState(false);
const [experiVal, setExperiVal] = useState(experiType); // 经验值类型
const [compareVal, setCompareVal] = useState(compareType); // 对比目标
const [valTake, setValTake] = useState(valTakeType); // 取值方式
const [realData, setRealData] = useState([]); // 原始数据
const [compareData, setCompareData] = useState([]); // 对比数据
const [highLimitVal, setHighLimitVal] = useState(highLimit); // 高限值
const [higherLimitVal, setHigherLimitVal] = useState(higherLimit); // 高限值
const [sensorData, setSensorData] = useState({ sensorName: sensors }); // 指标配置数据
const chartRef = useRef(null);
const dayStart = moment().format('yyyy-MM-DD 00:00:00');
const dayEnd = moment().format('yyyy-MM-DD 23:59:59');
const monthStart = moment().startOf('month').format('yyyy-MM-DD 00:00:00');
const monthEnd = moment().endOf('month').format('yyyy-MM-DD 23:59:59');
// 当前时间
const [dateFrom1, dateTo1] = useMemo(() => {
if (experiVal === 'h') {
return [dayStart, dayEnd];
} else {
return [monthStart, monthEnd];
}
}, [experiVal]);
// 对比时间
const [dateFrom2, dateTo2] = useMemo(() => {
// 小时-同比:今日0点-24点对应昨日0点-24点
if (experiVal === 'h' && compareVal === '同比') {
return [
moment(dayStart).subtract(1, 'days').format(FORMAT),
moment(dayEnd).subtract(1, 'days').format(FORMAT),
];
}
// 小时-环比:今日0点-24点对比当前时间往前推1小时
if (experiVal === 'h' && compareVal === '环比') {
return [
moment(dayStart).subtract(1, 'hours').format(FORMAT),
moment(dayEnd).subtract(1, 'hours').format(FORMAT),
];
}
// 日-同比:本月1日-31日对比上个月1日-31日
if (experiVal === 'day' && compareVal === '同比') {
return [
moment(monthStart).subtract(1, 'months').format(FORMAT),
moment(monthEnd).subtract(1, 'months').format(FORMAT),
];
}
// 日-环比:本月1日-31日对比当前时间往前推一天
if (experiVal === 'day' && compareVal === '环比') {
return [
moment(monthStart).subtract(1, 'days').format(FORMAT),
moment(monthEnd).subtract(1, 'days').format(FORMAT),
];
}
return ['', ''];
}, [experiVal, compareVal]);
//chart options
const options = useMemo(() => {
const _options = getOptions(
realData,
compareData,
experiVal,
compareVal,
highLimitVal,
higherLimitVal,
valTake,
sensorData,
);
return _options;
}, [realData, compareData, highLimitVal, higherLimitVal, , valTake, sensorData]);
// 确定
const onOk = () => {
onClose();
};
// 取消
const onCancel = () => {
onClose();
};
// 获取统计服务数据
const getStatisticsData = async () => {
try {
const params1 = {
pageIndex: 1,
pageSize: 1,
q_DeviceReports: [
{
accountName: deviceType,
nameTypeList: [
{
name: sensors,
type: sensors.includes('累计') ? 'Sub' : 'Avg',
},
],
dateType: experiVal,
dateFrom: dateFrom1,
dateTo: dateTo1,
deviceCode,
},
],
dateType: experiVal,
};
const params2 = _.cloneDeep(params1);
params2.q_DeviceReports[0].dateFrom = dateFrom2;
params2.q_DeviceReports[0].dateTo = dateTo2;
setLoading(true);
const req1 = getStatisticsInfo(params1);
const req2 = getStatisticsInfo(params2);
const results = await Promise.all([req1, req2]);
setLoading(false);
const [res1, res2] = results;
if (res1.code === 0 && res2.code === 0) {
let _realData = res1?.data?.list?.[0]?.dNameDataList?.[0]?.nameDate ?? [];
let _compareData = res2?.data?.list?.[0]?.dNameDataList?.[0]?.nameDate ?? [];
_realData = _realData.map((item) => ({
...item,
value: typeof item.value === 'number' ? item.value.toFixed(2) * 1 : 0,
}));
_compareData = _compareData.map((item) => ({
...item,
value: typeof item.value === 'number' ? item.value.toFixed(2) * 1 : 0,
}));
setRealData(_realData);
setCompareData(_compareData);
setSensorData({
...sensorData,
unit: res1?.data?.list?.[0]?.dNameDataList?.[0]?.unit ?? '',
});
} else {
setRealData([]);
setCompareData([]);
}
} catch (error) {
setLoading(false);
console.log(error);
}
};
useEffect(() => {
getStatisticsData();
}, [experiVal, dateFrom1, dateTo1, dateFrom2, dateTo2]);
return (
<>
<div className={classNames(`${prefixCls}`)}>经验报警</div>
<Modal
title={title}
centered
okText={'确定'}
width={width || '1200px'}
cancelText={'取消'}
open={visible}
onOk={onOk}
onCancel={onCancel}
wrapClassName={classNames(`${prefixCls}`)}
getContainer={getContainer || document.body}
>
<Spin spinning={loading}>
<div className={classNames(`${prefixCls}-box`)}>
<div className={classNames(`${prefixCls}-header`)}>
<div className={classNames(`${prefixCls}-header-list`)}>
<span className={classNames(`${prefixCls}-header-item`)}>
经验值类型:
<Radio.Group
options={experiTypeOptions}
optionType={'button'}
value={experiVal}
onChange={(e) => {
setExperiVal(e.target.value);
}}
/>
</span>
<span className={classNames(`${prefixCls}-header-item`)}>
对比目标:
<Radio.Group
options={compareOptions}
optionType={'button'}
value={compareVal}
onChange={(e) => {
setCompareVal(e.target.value);
}}
/>
</span>
<span className={classNames(`${prefixCls}-header-item`)}>
取值方式:
<Select options={valTakeOptions} value={valTake} onChange={setValTake} />
</span>
<span className={classNames(`${prefixCls}-header-item`)}>
偏差范围: 高限
<InputNumber
addonAfter={valTake === '按偏移率' ? '%' : ''}
style={{
marginLeft: '2px',
marginRight: '8px',
width: valTake === '按偏移率' ? '110px' : '90px',
}}
value={highLimitVal}
onChange={(val) => {
if (val && higherLimitVal && val > higherLimitVal)
return message.info('高限值不能大于或等于高高限值!');
setHighLimitVal(val);
}}
min={0}
/>
高高限
<InputNumber
addonAfter={valTake === '按偏移率' ? '%' : ''}
style={{ marginLeft: '2px', width: valTake === '按偏移率' ? '110px' : '90px' }}
value={higherLimitVal}
onChange={(val) => {
if (val && highLimitVal && val <= highLimitVal)
return message.info('高高限值不能小于或等于高限值!');
setHigherLimitVal(val);
}}
min={0}
/>
</span>
</div>
</div>
<div className={classNames(`${prefixCls}-content`)}>
<BasicChart
ref={chartRef}
option={options}
notMerge
style={{ width: '100%', height: '100%' }}
/>
</div>
</div>
</Spin>
</Modal>
</>
);
};
export default EmpiricalCurve;
export default PredictionCurve;
......@@ -3,5 +3,50 @@
@tree-custom-prefix-cls: ~'@{ant-prefix}-empirical-curve';
.@{tree-custom-prefix-cls} {
background: red;
.@{ant-prefix}-modal-body {
height: 650px;
.@{ant-prefix}-spin-nested-loading{
height: 100%;
.@{ant-prefix}-spin-container{
height: 100%;
}
}
}
&-box {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
&-header {
flex: none;
&-list {
display: flex;
align-items: center;
width: 100%;
&:last-of-type {
margin-top: 10px;
}
}
&-item {
margin-right: 20px;
display: flex;
align-items: center;
justify-content: center;
.@{ant-prefix}-select {
width: 160px;
}
}
}
&-content {
flex: 1;
overflow: hidden;
}
}
\ No newline at end of file
import moment from 'moment';
const CHART_FORMAT = 'yyyy-MM-DD HH';
const COLOR = {
NORMAL: '#1685FF',
UPER: '#fa8c16',
UPUPER: '#FF0000',
LOWER: '#fa8c16',
LOWLOWER: '#FF0000',
AVG: '#00B8B1',
COMPARE: '#00b809',
};
const dealCompareData = (compareData, experiVal, compareVal) => {
let newData = [...compareData];
// 根据经验值类型和对比目标处理compareData
if (experiVal === 'h' && compareVal === '同比') {
newData = newData.map((item) => [
moment(item.occurrenceTime).add(1, 'days').format(CHART_FORMAT),
item.value,
]);
}
if (experiVal === 'h' && compareVal === '环比') {
newData = newData.map((item) => [
moment(item.occurrenceTime).add(1, 'hours').format(CHART_FORMAT),
item.value,
]);
}
if (experiVal === 'day' && compareVal === '同比') {
newData = newData.map((item) => [
moment(item.occurrenceTime).add(1, 'months').format(CHART_FORMAT),
item.value,
]);
}
if (experiVal === 'day' && compareVal === '环比') {
newData = newData.map((item) => [
moment(item.occurrenceTime).add(1, 'days').format(CHART_FORMAT),
item.value,
]);
}
return newData;
};
const getLabelTime = (time, experiVal, compareVal) => {
let newTime = time;
if (experiVal === 'h' && compareVal === '同比') {
newTime = moment(time).subtract(1, 'days').format(CHART_FORMAT);
}
if (experiVal === 'h' && compareVal === '环比') {
newTime = moment(time).subtract(1, 'hours').format(CHART_FORMAT);
}
if (experiVal === 'day' && compareVal === '同比') {
newTime = moment(time).subtract(1, 'months').format(CHART_FORMAT);
}
if (experiVal === 'day' && compareVal === '环比') {
newTime = moment(time).subtract(1, 'days').format(CHART_FORMAT);
}
return newTime;
};
const grid = {
top: 80,
left: 30,
right: 10,
bottom: 60,
containLabel: true,
};
const yAxis = {
type: 'value',
axisLine: {
show: true,
},
minorTick: {
lineStyle: {
color: '#e2e2e2',
},
show: true,
splitNumber: 2,
},
minorSplitLine: {
lineStyle: {
color: '#e2e2e2',
type: 'dashed',
},
show: true,
},
splitLine: {
show: true,
},
// min: 'dataMin',
// max: 'dataMax',
};
const dataZoom = [
{
type: 'slider',
filterMode: 'weakFilter',
showDataShadow: false,
labelFormatter: '',
},
{
type: 'inside',
filterMode: 'weakFilter',
},
];
const toFixedVal = (val, decimal = 2) => {
if (val === null || val === undefined) return 0;
return val.toFixed(decimal) * 1;
};
// 将原始值处理成包含高限、高高限
const dealCompeletedData = (realData, highLimit, higherLimit, valTake) => {
const dataSource = [...realData];
if (dataSource.length === 0) return [];
if (valTake === '按偏移量') {
// 存在高低限
if (highLimit && higherLimit) {
dataSource.forEach((item) => {
item.real_lower = toFixedVal(item.value - higherLimit);
item.real_low = toFixedVal(item.value - highLimit);
item.real_uper = toFixedVal(item.value + highLimit);
item.real_upuper = toFixedVal(item.value + higherLimit);
item.lower = item.real_lower;
item.low = toFixedVal(item.real_low - item.real_lower);
item.uper = toFixedVal(item.real_uper - item.real_low);
item.upuper = toFixedVal(item.real_upuper - item.real_uper);
});
}
//只存在高限
if (highLimit && !higherLimit) {
dataSource.forEach((item) => {
item.real_low = toFixedVal(item.value - highLimit);
item.real_uper = toFixedVal(item.value + highLimit);
item.low = item.real_low;
item.uper = toFixedVal(item.real_uper - item.real_low);
});
}
// 只存在高高限
if (!highLimit && higherLimit) {
dataSource.forEach((item) => {
item.real_lower = toFixedVal(item.value - higherLimit);
item.real_upuper = toFixedVal(item.value + higherLimit);
item.lower = item.real_lower;
item.upuper = toFixedVal(item.real_upuper - item.real_uper);
});
}
} else {
if (highLimit && higherLimit) {
dataSource.forEach((item) => {
item.real_lower = toFixedVal(item.value - higherLimit * 0.01 * item.value);
item.real_low = toFixedVal(item.value - highLimit * 0.01 * item.value);
item.real_uper = toFixedVal(item.value + highLimit * 0.01 * item.value);
item.real_upuper = toFixedVal(item.value + higherLimit * 0.01 * item.value);
item.lower = item.real_lower;
item.low = toFixedVal(item.real_low - item.real_lower);
item.uper = toFixedVal(item.real_uper - item.real_low);
item.upuper = toFixedVal(item.real_upuper - item.real_uper);
});
}
if (highLimit && !higherLimit) {
dataSource.forEach((item) => {
item.real_low = toFixedVal(item.value - highLimit * 0.01 * item.value);
item.real_uper = toFixedVal(item.value + highLimit * 0.01 * item.value);
item.low = item.real_low;
item.uper = toFixedVal(item.real_uper - item.real_low);
});
}
if (!highLimit && higherLimit) {
dataSource.forEach((item) => {
item.real_lower = toFixedVal(item.value - higherLimit * 0.01 * item.value);
item.real_upuper = toFixedVal(item.value + higherLimit * 0.01 * item.value);
item.lower = item.real_lower;
item.upuper = toFixedVal(item.real_upuper - item.real_uper);
});
}
}
return dataSource;
};
// 获取低限、高限、低低限、高高限数据
const getLimitData = (limitType, item) => {
const occurrenceTime = moment(item.occurrenceTime).format(CHART_FORMAT);
return limitType ? [occurrenceTime, item[limitType]] : [];
};
const getRealLimitVal = (item, data) => {
const { seriesName, dataIndex } = item;
let value = item.value;
if (seriesName === '低限') {
value = data[dataIndex].real_low;
} else if (seriesName === '高限') {
value = data[dataIndex].real_uper;
} else if (seriesName === '低低限') {
value = data[dataIndex].real_lower;
} else if (seriesName === '高高限') {
value = data[dataIndex].real_upuper;
}
return value;
};
const getMinMaxValue = (data, highLimit, higherLimit) => {
let minValue = 0,
maxValue = 0;
if (higherLimit) {
minValue = Math.min(...data.map((item) => item.real_lower));
maxValue = Math.max(...data.map((item) => item.real_upuper));
} else if (highLimit) {
minValue = Math.min(...data.map((item) => item.real_low));
maxValue = Math.max(...data.map((item) => item.real_uper));
} else {
minValue = Math.min(...data.map((item) => item.value));
maxValue = Math.max(...data.map((item) => item.value));
}
return [minValue, maxValue];
};
const getOptions = (
realData = [],
compareData = [],
experiVal,
compareVal, // 对比值
highLimit, // 高限
higherLimit, // 高高限
valTake, // 取值方式
sensorData, // 指标配置数据
) => {
const { sensorName, unit = '' } = sensorData;
// 根据偏差值算出低限、高限、低低限、高高限
const compeletedData = dealCompeletedData(realData, highLimit, higherLimit, valTake);
// const [min, max] = getMinMaxValue(compeletedData, highLimit, higherLimit);
// 原始数据
const realDataArr = realData.map((item) => [
moment(item.occurrenceTime).format(CHART_FORMAT),
item.value,
]);
// 对比数据
const compareDataArr = dealCompareData(compareData, experiVal, compareVal);
//高限、高高限数据
const lowLimitArr = highLimit ? compeletedData.map((item) => getLimitData('low', item)) : [];
const highLimitArr = highLimit ? compeletedData.map((item) => getLimitData('uper', item)) : [];
const lowerLimitArr = higherLimit
? compeletedData.map((item) => getLimitData('upuper', item))
: [];
const higherLimitArr = higherLimit
? compeletedData.map((item) => getLimitData('lower', item))
: [];
let options = {
grid,
tooltip: {
trigger: 'axis',
show: true,
formatter: (params) => {
let result = '';
params.forEach((item) => {
const labelTime = `${
item.seriesName === '对比值'
? getLabelTime(item.value[0], experiVal, compareVal)
: item.value[0]
}点`;
const seriesName = item.seriesName;
const value =
item.seriesIndex > 1 ? getRealLimitVal(item, compeletedData) : item.value[1];
result += `<span>${item.seriesIndex > 1 ? seriesName : labelTime} : <b style="color: ${
item.color
}">${value}</b> <span style="font-size: 12px">${unit}</span> </span><br />`;
});
return result;
},
},
legend: {
show: true,
selectedMode: 'multiple',
data: ['原始值', '对比值'],
},
xAxis: {
type: 'time',
// axisLabel: {
// formatter: experiVal
// ? experiVal === 'day'
// ? '{dd}日'
// : '{HH}:{mm}'
// : {
// year: '{yyyy}',
// month: '{MMM}',
// day: '{MMM}{d}日',
// hour: '{HH}:{mm}',
// minute: '{HH}:{mm}',
// second: '{HH}:{mm}:{ss}',
// none: '{yyyy}-{MM}-{dd} {hh}:{mm}:{ss}',
// },
// },
minorTick: {
lineStyle: {
color: '#e2e2e2',
},
show: true,
splitNumber: 2,
},
minorSplitLine: {
lineStyle: {
color: '#e2e2e2',
type: 'dashed',
},
show: true,
},
splitLine: {
show: true,
},
minInterval: 3600 * 1000,
},
yAxis,
dataZoom,
series: [
{
name: '原始值',
type: 'line',
smooth: true,
symbol: 'none',
itemStyle: {
color: COLOR.NORMAL,
},
data: realDataArr,
},
{
name: '对比值',
type: 'line',
smooth: true,
symbol: 'none',
itemStyle: {
color: COLOR.COMPARE,
},
data: compareDataArr,
},
],
};
/*生成泳道图 高限*/
[[...lowLimitArr], [...highLimitArr]].forEach((item, index) => {
options.series.push({
name: index === 0 ? '低限' : '高限',
type: 'line',
itemStyle: {
color: index === 0 ? COLOR.LOWER : COLOR.UPER,
},
data: item,
lineStyle: {
opacity: 0,
},
...(index !== 0
? {
areaStyle: {
color: options.series?.[0]?.itemStyle?.color ?? '#65a0d1',
opacity: 0.2,
},
}
: {}),
stack: 'confidence-band',
symbol: 'none',
});
});
/*生成泳道图 高高限*/
[[...lowerLimitArr], [...higherLimitArr]].forEach((item, index) => {
options.series.push({
name: index === 0 ? '低低限' : '高高限',
type: 'line',
itemStyle: {
color: index === 0 ? COLOR.LOWLOWER : COLOR.UPUPER,
},
data: item,
lineStyle: {
opacity: 0,
},
...(index !== 0
? {
areaStyle: {
color: 'red',
opacity: 0.2,
},
}
: {}),
stack: 'confidence-band',
symbol: 'none',
});
});
return options;
};
const experiTypeOptions = [
{
label: '小时',
value: 'h',
},
{
label: '日',
value: 'day',
},
];
const compareOptions = [
{
label: '同比',
value: '同比',
},
{
label: '环比',
value: '环比',
},
];
const valTakeOptions = [
{
label: '按偏移率',
value: '按偏移率',
},
{
label: '按偏移量',
value: '按偏移量',
},
];
export {
dealCompareData,
getLabelTime,
getOptions,
experiTypeOptions,
compareOptions,
valTakeOptions,
};
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment