import React, { useContext, useEffect, useRef, useState } from 'react'; import { ConfigProvider, Modal, Radio, Slider, InputNumber, Input, Button, Checkbox } from 'antd'; import classNames from 'classnames'; import moment from 'moment'; import { BasicChart } from '@wisdom-components/basicchart'; import { getHistoryInfo } from '../../apis'; import { std } from 'mathjs'; import skmeans from 'skmeans'; import { outlierArr, timeArr, chartArr, average, markArr, median } from '../utils'; import './index.less'; const IntellectDraw = (props) => { const { getPrefixCls } = useContext(ConfigProvider.ConfigContext); const prefixCls = getPrefixCls('intellect-draw'); const { deviceCode, sensors, deviceType, changeSpin } = props; const [open, setOpen] = useState(false); const [outlier, setOutlier] = useState(3); // 过滤异常 const [sensorData, setSensorData] = useState([]); // 所有数据 const [chartData, setChartData] = useState([]); // 图表数据 const [timeCycle, setTimeCycle] = useState(60); const [timeDan, setTimeDan] = useState(1); const [foldData, setFoldData] = useState([]); const [options, setOptions] = useState({}); const [selectCheck, setSelectCheck] = useState([]); const chartRef = useRef(null); // 获取历史数据 const getSensorsData = async () => { changeSpin(true); const params = { isDilute: true, zoom: '5', unit: 'min', dateFrom: moment().subtract(8, 'day').format('YYYY-MM-DD 00:00:00'), dateTo: moment().subtract(1, 'day').format('YYYY-MM-DD 23:59:59'), acrossTables: [{ deviceCode: deviceCode, sensors: sensors, deviceType: deviceType }], isBoxPlots: true, ignoreOutliers: true, }; const results = await getHistoryInfo(params); changeSpin(false); const historyData = results?.data?.[0] || {}; setSensorData(() => { return historyData; }); }; // 图表数据处理 const chartDataHandle = (data) => { const times = moment().subtract(1, 'day').format('YYYY-MM-DD'); const chart = data.map((item) => { return { ...item, time: moment(item.pt).format(times + ' HH:mm:ss'), }; }); return chart; }; // 聚集方法 const clusteredMothod = (clustered, num) => { // console.log(clustered); let foldLine = []; const arr = []; const times = moment().subtract(1, 'day').format('YYYY-MM-DD'); const data = clustered.map((item) => { return item[1]; }); const medianVal = median([...data]); // console.log(data, medianVal); if (timeDan === 1) { return [ [new Date(moment(times + ' 00:00:00')).getTime(), medianVal], [new Date(moment(times + ' 23:59:59')).getTime(), medianVal], ]; } data.forEach((item, index) => { if ( index === 0 || (medianVal < item && medianVal > data[index - 1] && index > 0) || (medianVal > item && medianVal < data[index - 1] && index > 0) ) { arr.push([ { x: clustered[index][0], y: clustered[index][1], l: item, }, ]); } else { arr[arr.length - 1].push({ x: clustered[index][0], y: clustered[index][1], l: item, }); } }); // console.log(arr); arr.forEach((list, index) => { const _list = list.map((item) => { return item['y']; }); const _medianVal = median([..._list]); if (index === 0) { foldLine = foldLine.concat([ [new Date(moment(times + ' 00:00:00')).getTime(), _medianVal], [list[list.length - 1].x, _medianVal], ]); } else if (index === arr.length - 1) { foldLine = foldLine.concat([ [arr[index - 1].at(-1).x, _medianVal], [new Date(moment(times + ' 23:59:59')).getTime(), _medianVal], ]); } else { foldLine = foldLine.concat([ [arr[index - 1].at(-1).x, _medianVal], [list[list.length - 1].x, _medianVal], ]); } }); // console.log(foldLine, 'foldLine'); if (arr.length === timeDan || arr.length === num) { return foldLine; } else { return clusteredMothod(foldLine, arr.length); } }; // 渲染图表 const renderChart = (_chartData, _clustered) => { const chartDatas = _chartData.map((item) => { return [new Date(item.time).getTime(), item.pv]; }); const clustered = skmeans(chartDatas, 24); const { centroids = [] } = clustered; const _centroids = centroids.sort((a, b) => { return a[0] - b[0]; }); // console.log(_centroids); const foldLine = clusteredMothod(_centroids, 0); // console.log(foldLine); const option = { xAxis: { type: 'time', axisTick: { alignWithLabel: true, }, boundaryGap: false, splitLine: { show: true, lineStyle: { type: 'dashed', }, }, }, yAxis: { type: 'value', name: sensorData?.unit || '', position: 'left', alignTicks: true, axisLine: { show: true, }, axisLabel: { formatter: '{value}', }, }, series: [ { type: 'scatter', name: sensors, sampling: 'average', large: true, symbolSize: 5, data: _chartData.map((item) => { return [new Date(item.time).getTime(), item.pv]; }), }, { type: 'line', name: '趋势', sampling: 'average', large: true, data: _centroids.map((item) => { return [Math.floor(item[0]), item[1]]; }), }, { type: 'line', name: '限值', data: foldLine.map((item) => { return [Math.floor(item[0]), item[1]]; }), }, ], }; setOptions(option); setFoldData(foldDataMethod(foldLine)); }; // 限值数据处理 const foldDataMethod = (data) => { let _data = []; data.forEach((item, index) => { if (index % 2 === 0) { _data = _data.concat([[item]]); } else { _data[Math.floor(index / 2)].push([...item]); } }); // console.log(_data); return _data.map((list) => { return { start: moment(list[0][0]).format('HH:mm'), end: moment(list[1][0]).format('HH:mm'), value: list[0][1], wave: 10, }; }); }; const onCheckChange = (checkedValues) => { setSelectCheck(checkedValues); }; const proposeRender = () => { return ( <div className={classNames(`${prefixCls}-propose-box`)}> <Checkbox.Group onChange={onCheckChange}> {foldData.map((list, index) => { const decimalPoint = sensorData?.decimalPoint || 2; const lower1 = (list.value * (1 - list.wave / 100) * (1 - list.wave / 100)).toFixed(decimalPoint) * 1; const lower2 = (list.value * (1 - list.wave / 100)).toFixed(decimalPoint) * 1; const high1 = (list.value * (1 + list.wave / 100)).toFixed(decimalPoint) * 1; const high2 = (list.value * (1 + list.wave / 100) * (1 + list.wave / 100)).toFixed(decimalPoint) * 1; return ( <div className={classNames(`${prefixCls}-propose-list`)} key={index}> <div className={classNames(`${prefixCls}-propose-select`)}> <Checkbox value={index}> {list.start}-{list.end} </Checkbox> </div> <div className={classNames(`${prefixCls}-propose-value`)}> <div className={classNames(`${prefixCls}-value-list`)}> <Input style={{ width: '150px', }} addonBefore="低低限" value={lower1} disabled /> </div> <div className={classNames(`${prefixCls}-value-list`)}> <Input style={{ width: '150px', }} addonBefore="低限" value={lower2} disabled /> </div> <div className={classNames(`${prefixCls}-value-list`)}> <Input style={{ width: '150px', }} addonBefore="高限" value={high1} disabled /> </div> <div className={classNames(`${prefixCls}-value-list`)}> <Input style={{ width: '150px', }} addonBefore="高高限" value={high2} disabled /> </div> </div> <div className={classNames(`${prefixCls}-propose-range`)}> <span className={classNames(`${prefixCls}-label`)}>允许浮动范围:</span> <Slider min={0} max={100} style={{ width: '100px' }} onChange={(value) => { const _foldData = structuredClone(foldData); _foldData[index].wave = value; setFoldData(_foldData); }} value={typeof list.wave === 'number' ? list.wave : 0} /> <InputNumber min={1} max={100} style={{ margin: '0 16px', width: '100px', }} formatter={(value) => `${value}%`} value={list.wave} onChange={(value) => { const _foldData = structuredClone(foldData); _foldData[index].wave = value; setFoldData(_foldData); }} /> </div> </div> ); })} </Checkbox.Group> </div> ); }; useEffect(() => { const data = []; const decimalPoint = sensorData?.decimalPoint || 2; selectCheck.forEach((index) => { const value = foldData[index]?.value || 0; const wave = foldData[index]?.wave || 0; const lower1 = (value * (1 - wave / 100) * (1 - wave / 100)).toFixed(decimalPoint) * 1; const lower2 = (value * (1 - wave / 100)).toFixed(decimalPoint) * 1; const high1 = (value * (1 + wave / 100)).toFixed(decimalPoint) * 1; const high2 = (value * (1 + wave / 100) * (1 + wave / 100)).toFixed(decimalPoint) * 1; data.push({ ...foldData[index], lower1, lower2, high1, high2, }) }) if (data.length) return props.backData([data[0].lower1, data[0].lower2, data[0].high1, data[0].high2]); return props.backData([]); // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectCheck, foldData]); useEffect(() => { open && getSensorsData(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [open]); useEffect(() => { const { dataModel = [] } = sensorData; if (!dataModel.length) return setChartData([]); const count = Math.floor((24 * 60) / timeCycle); const times = moment().subtract(1, 'day').format('YYYY-MM-DD'); const _dataModel = dataModel.map((item) => { return { ...item, time: moment(item.pt).format(times + ' HH:mm:ss'), }; }); const _chartData = []; const _clustered = []; for (let i = 0; i < count; i++) { const data = _dataModel.filter((item) => { const time = new Date(item.time).getTime(); const min = new Date(moment(times + ' 00:00:00').add(i * timeCycle, 'minute')).getTime(); const max = new Date( moment(times + ' 00:00:00').add((i + 1) * timeCycle, 'minute'), ).getTime(); return time && time >= min && max >= time; }); let dataArr = []; const pvArr = data.map((item) => { return item.pv || 0; }); const stdVal = pvArr.length ? std(pvArr) : 0; const medianVal = pvArr.length ? average(pvArr) : 0; const range = { min: medianVal - outlier * stdVal, max: medianVal + outlier * stdVal, }; data.forEach((item) => { if (item.pv >= range.min && item.pv <= range.max) dataArr.push(item); }); if (!outlier) dataArr = [].concat([...data]); _chartData.push(...dataArr); } renderChart(_chartData, _clustered); // eslint-disable-next-line react-hooks/exhaustive-deps }, [sensorData, outlier, timeDan]); useEffect(() => { setOpen(props.open); }, [props.open]); return ( <> <div className={classNames(`${prefixCls}`)}> <div className={classNames(`${prefixCls}-header`)}> <div className={classNames(`${prefixCls}-list`)}> <span className={classNames(`${prefixCls}-item`)}> <span className={classNames(`${prefixCls}-label`)}>异常值剔除:</span> <Slider marks={markArr} step={null} style={{ width: '200px', }} defaultValue={1} onChange={(value) => { setOutlier(outlierArr[value]?.value || 0); }} min={0} max={3} tooltip={{ formatter: (value) => { return markArr[value]; }, }} /> </span> <span className={classNames(`${prefixCls}-item`)}> <span className={classNames(`${prefixCls}-label`)}>限值时段个数:</span> <InputNumber min={1} max={10} style={{ width: '100px', }} value={timeDan} onChange={(value) => { setTimeDan(value); }} /> </span> </div> <div className={classNames(`${prefixCls}-propose`)}> <span className={classNames(`${prefixCls}-label`)}>建议限值:</span> {proposeRender()} </div> </div> <div className={classNames(`${prefixCls}-chart`)}> <BasicChart ref={chartRef} option={options} notMerge style={{ width: '100%', height: '100%' }} /> </div> </div> </> ); }; export default IntellectDraw;