GridChart.js 23.7 KB
Newer Older
1 2 3 4 5
/**
 * @tips:
 * 2024年12月10日 当能查到数据,但是数据都是null,则图表的分割线无法正常出来。
 *
 * */
6
import React, {memo, useEffect, useMemo, useRef, useState} from 'react';
7
import _, {cloneDeep} from 'lodash';
8
import {BasicChart} from '@wisdom-components/basicchart';
9
import PandaEmpty from '@wisdom-components/empty';
10
import optionGenerator from './utils';
11 12
import {getPointAddress, getPointAddressEntry, getSensorsRealName, getSensorType, getStatisticsInfo} from "./apis";
import moment from "moment";
13

14 15 16 17 18 19 20 21
const ChartTitle = ({prefixCls, title, unit}) => {
    const cls = `${prefixCls}-grid-item-title`;
    return (
        <div className={cls}>
            <span className={`${cls}_text`}>{title}</span>
            {unit && <span className={`${cls}_unit`}>(单位:{unit}</span>}
        </div>
    );
22
};
23 24 25 26 27
const ChartWidthRef = (props) => {
    const ref = useRef(null);
    const timerRef = useRef(null);
    const minMaxMarkPoint = (dataSource, chart, isInit) => {
        // 只有一个数据曲线时显示markline
28
        let isMultiple = props.isMultiple
29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85
        if (dataSource.length !== 1) return {};
        // 1. 找出最大、最小的值
        let pointArr = dataSource[0].dataModel;
        let valueArr = pointArr.map(item => item.pv);
        let maxValue = Math.max(...valueArr);
        let minValue = Math.min(...valueArr);
        // 2. 找出点的索引和实际的点
        let maxValueIndex = valueArr.findIndex(val => val === maxValue);
        let minValueIndex = valueArr.findIndex(val => val === minValue);
        let maxPoint = pointArr[maxValueIndex];
        let minPoint = pointArr[minValueIndex];
        if (!maxPoint || !minPoint) return {}
        // 3. 通过最大值、最小值,数组的首值尾值以及图表宽度来确认markpoint的位置
        let _opts = chart.getOption();
        let zoom = _opts.dataZoom[0];
        let startPoint = pointArr[0];
        let endPoint = pointArr[pointArr.length - 1];
        let timePeriod = isInit ? moment(endPoint.pt) - moment(startPoint.pt) : zoom.endValue - zoom.startValue;
        let chartWidth = chart.getWidth();
        // 需要考虑是否为0的情况
        // 4. 计算最大最小值的标签宽度
        let maxLength = 70 + String(maxValue).length * 5;
        let minLength = 70 + String(minValue).length * 5;
        // 5. 确定是否超出边界,确定超出边界是哪一边;
        // 用首尾时间评分
        let gapOfYAxisToEdge = 62;
        let startTime = isInit ? moment(startPoint.pt) : zoom.startValue;
        let maxPointPosition = ((chartWidth - gapOfYAxisToEdge) / timePeriod) * (moment(maxPoint.pt).valueOf() - startTime) + gapOfYAxisToEdge;
        let minPointPosition = ((chartWidth - gapOfYAxisToEdge) / timePeriod) * (moment(minPoint.pt).valueOf() - startTime) + gapOfYAxisToEdge;
        let maxTagLeft = maxPointPosition - maxLength / 2;
        let maxTagRight = maxPointPosition + maxLength / 2;
        let minTagLeft = minPointPosition - minLength / 2;
        let minTagRight = minPointPosition + minLength / 2;

        let maxOutEdge = false, minOutEdge = false;
        let maxOutSide = '', minOutSide = '';
        // 在实际使用中,我们认为不存在一个tag同时超出边界的情况。
        if (maxTagLeft < 0) {
            maxOutEdge = true;
            maxOutSide = 'left';
        }
        if (maxTagRight > chartWidth) {
            maxOutEdge = true;
            maxOutSide = 'right';
        }

        if (minTagLeft < 0) {
            minOutEdge = true;
            minOutSide = 'left';
        }
        if (minTagRight > chartWidth) {
            minOutEdge = true;
            minOutSide = 'right';
        }
        // 6. 确定使用的图形
        // 默认图形,居中
        let maxIconPath = `path://M1233.329493 195.75466633h-1112.064c-34.128213 0-61.781333 27.661653-61.781333 61.781334v473.658026c0 34.117973 27.65312 61.781333 61.781333 61.781334h472.80128l83.23072 144.155306 83.23072-144.155306h472.80128c34.128213 0 61.781333-27.66336 61.781334-61.781334V257.53600033c0-34.117973-27.65312-61.781333-61.781334-61.781334z`;
86 87
        let maxSymbolOffset = isMultiple ? [0, -14] : [0, -20];
        let _maxOffset = isMultiple ? -14 : -20;
88 89 90
        if (maxOutEdge) {
            if (maxOutSide === 'left') {
                // 左边界超出,使用朝右的图表
91
                maxSymbolOffset = ['50%', _maxOffset];
92 93 94
                maxIconPath = 'path://M48.677,6.151v10c0,1.7-1.3,3-3,3h-35.5l-6.7,4c0,0,0.2-5.3,0.2-7v-10c0-1.7,1.3-3,3-3h39C47.277,3.151,48.677,4.551,48.677,6.151z';
            } else if (maxOutSide === 'right') {
                // 右边界超出,使用朝左的图表
95
                maxSymbolOffset = ['-50%', _maxOffset];
96 97 98 99 100
                maxIconPath = 'path://M6.477,3.151h39c1.7,0,3,1.3,3,3v10c0,1.7,0.2,7,0.2,7l-6.7-4h-35.5c-1.7,0-3-1.3-3-3v-10C3.477,4.551,4.877,3.151,6.477,3.151z'
            }
        }
        // 默认图形
        let minIconPath = 'path://M131.999 849.579h1112.064c34.128 0 61.781-27.662 61.781-61.781v-473.658c0-34.118-27.653-61.781-61.781-61.781h-472.801l-83.231-144.155-83.231 144.155h-472.801c-34.128 0-61.781 27.663-61.781 61.781v473.658c0 34.118 27.653 61.781 61.781 61.781z';
101 102
        let minSymbolOffset = isMultiple ? [0, 14] : [0, 20];
        let _minOffset = isMultiple ? 14 : 20;
103 104 105
        if (minOutEdge) {
            if (minOutSide === 'left') {
                // 左边界超出,使用朝右的图表
106
                minSymbolOffset = ['50%', _minOffset];
107 108 109
                minIconPath = 'path://M45.677,23.151h-39c-1.7,0-3-1.3-3-3v-10c0-1.7-0.2-7-0.2-7l6.7,4h35.5c1.7,0,3,1.3,3,3v10C48.677,21.751,47.277,23.151,45.677,23.151z';
            } else if (minOutSide === 'right') {
                // 右边界超出,使用朝左的图表
110
                minSymbolOffset = ['-50%', _minOffset];
111 112 113 114 115 116 117 118 119 120 121 122 123
                minIconPath = 'path://M3.477,20.151v-10c0-1.7,1.3-3,3-3h35.5l6.7-4c0,0-0.2,5.3-0.2,7v10c0,1.7-1.3,3-3,3h-39C4.877,23.151,3.477,21.751,3.477,20.151z'
            }
        }

        const data = [
            {
                type: 'min',
                color: 'rgba(255,255,255,1)',
                name: '最小: ',
                symbolOffset: minSymbolOffset,
                symbol: minIconPath,
                symbolSize: (e) => {
                    let str = ![undefined, null].includes(e) ? String(e) : '';
124 125
                    let length = (isMultiple ? 40 : 60) + str.length * 6
                    return [length, isMultiple ? 20 : 32]
126 127 128 129 130
                },
                label: {
                    show: true,
                    color: '#fff',
                    formatter: '最小: {c}',
131 132
                    fontSize: isMultiple ? 12 : 14,
                    fontWeight: isMultiple ? 'normal' : 'bold',
133 134 135 136 137 138 139 140 141 142 143 144 145 146 147
                    verticalAlign: 'top',
                    offset: [0, -2]
                },
                itemStyle: {
                    // color: "#21c8c3",
                }
            },
            {
                type: 'max',
                name: '最大: ',
                position: [20, 200],
                symbol: maxIconPath,
                symbolOffset: maxSymbolOffset,
                symbolSize: (e) => {
                    let str = ![undefined, null].includes(e) ? String(e) : '';
148 149
                    let length = (isMultiple ? 40 : 60) + str.length * 6
                    return [length, isMultiple ? 20 : 32]
150 151 152 153 154 155
                },
                itemStyle: {
                    // color: "#1980ff",
                },
                label: {
                    show: true,
156
                    color: '#fff',
157
                    formatter: '最大: {c}',
158 159 160 161
                    fontSize: isMultiple ? 12 : 14,
                    fontWeight: isMultiple ? 'normal' : 'bold',
                    offset: [0, -2]
                },
162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198
            },
            {
                name: '',
                type: 'max',
                symbol: 'emptyCircle',
                label: {show: false},
                symbolSize: 6,
            },
            {
                name: '',
                type: 'min',
                symbol: 'emptyCircle',
                label: {show: false},
                symbolSize: 6,
            }
        ];
        return {
            symbol: 'circle',
            symbolSize: 20,
            animation: false,
            silent: true,
            label: {
                show: false,
            },
            data,
        };
    };

    const renderMarkPoint = (isInit) => {
        if (timerRef.current) clearTimeout(timerRef.current);
        const chart = ref.current?.getEchartsInstance?.();
        timerRef.current = setTimeout(() => {
            chart.setOption({
                series: {markPoint: minMaxMarkPoint(props.data.list, chart, isInit)}
            })
        }, 200)
    };
陈龙's avatar
陈龙 committed
199 200
    // 将opt修改一下
    const cur_opt = useMemo(() => {
201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224
        let _option = cloneDeep(props.option);
        const chart = ref.current?.getEchartsInstance?.();
        if (_option?.series?.[0] && chart) {
            _option.series[0].markPoint = minMaxMarkPoint(props.data.list, chart, true);
            _option.series[0].markLine = {
                silent: false,
                symbol: 'none',
                data: [{
                    name: '平均线',
                    type: 'average',
                    lineStyle: {
                        color: '#00b8b1',
                        type: 'solid',
                    },
                    label: {
                        position: 'insideEndTop',
                        color: '#00b8b1',
                        formatter: function (param) {
                            return `平均值:${param.value}`;
                        },
                    },
                }]
            }
        }
陈龙's avatar
陈龙 committed
225
        return {
226 227
            ..._option, grid: {
                ...props.option.grid,
陈龙's avatar
陈龙 committed
228 229 230 231
                top: 45,
                bottom: 65,
            }
        }
232
    }, [props.option, ref, props.data.list]);
233 234

    useEffect(() => {
235
        ref.current?.resize?.();
236
        if (props.data.list?.length !== 1) return;
237

238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272
        const chart = ref.current?.getEchartsInstance?.();
        chart.setOption({
            series: {
                markLine: {
                    silent: false,
                    symbol: 'none',
                    data: [{
                        name: '平均线',
                        type: 'average',
                        lineStyle: {
                            color: '#00b8b1',
                            type: 'solid',
                        },
                        label: {
                            position: 'insideEndTop',
                            color: '#00b8b1',
                            formatter: function (param) {
                                return `平均值:${param.value}`;
                            },
                        },
                    }]
                }
            }
        })

        function dataZoomFn() {
            renderMarkPoint(false)
        }

        chart.on('legendselectchanged', renderMarkPoint);
        chart.on('datazoom', dataZoomFn);
        return () => {
            chart.off('legendselectchanged', renderMarkPoint);
            chart.off('datazoom', dataZoomFn);
        }
273
    }, [JSON.stringify(cur_opt)]);
274 275 276
    return <BasicChart
        ref={ref}
        {...props}
陈龙's avatar
陈龙 committed
277
        option={cur_opt}
278 279
    />
}
280
const GridChart = memo((props) => {
281 282 283 284 285 286
    const {
        dataSource,
        contrast = false,
        contrastOption = 'day',
        smooth = true,
        curveCenter,
287 288 289
        allPointAddress,
        allSensorType,
        dateRange
290 291
    } = props;
    const {prefixCls} = props;
292 293 294
    const [gridData, setGridData] = useState([]);
    const [pointAddressEntryData, setPointAddressEntryData] = useState(null);
    const [sensorType, setSensorType] = useState(null);
陈龙's avatar
陈龙 committed
295
    const [isInit, setIsInit] = useState(true);
296
    // 新增逻辑:需要区分出哪些是统计值
297

298 299 300 301 302 303
    /**
     *  @param {array} dataSource
     */
    const handleDataSource = async (dataSource) => {
        props.setLoading(true);
        // 1. 统计设备
304 305 306 307 308 309 310 311 312 313 314
        try {
            let _deviceTypes = [];
            let _deviceCodes = dataSource.reduce((final, cur) => {
                if (!final.includes(cur.stationCode) && !_deviceTypes.includes(cur.deviceType)) {
                    final.push(cur.stationCode);
                    _deviceTypes.push(cur.deviceType);
                }
                return final;
            }, []);
            // 2. 获取对应的版本id
            let _ids = [];
315 316
            let _idRequest = await getPointAddress({code: _deviceCodes.join(',')});
            _ids = _idRequest?.data ?? [];
317 318 319 320 321 322
            // 3. 获取对应的点表
            let _map = {};
            for await (let item of _ids) {
                let _index = _deviceCodes.findIndex(code => code === item.code);
                if (pointAddressEntryData && pointAddressEntryData[item.id]) {
                    _map[_deviceTypes[_index]] = pointAddressEntryData[item.id];
323
                } else {
324 325 326
                    let _entry = await getPointAddressEntry({versionId: item.id});
                    _map[_deviceTypes[_index]] = _entry?.data ?? [];
                    setPointAddressEntryData({...pointAddressEntryData, [item.id]: _entry?.data})
327 328
                }
            }
329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355
            // 4. 获取点类型
            let _sensorType = []
            if (sensorType) {
                _sensorType = sensorType;
            } else {
                _sensorType = (await getSensorType())?.data ?? [];
            }
            //5. 找出统计值,合并
            let _dataSource = cloneDeep(dataSource);
            let _nameListMap = {};
            let _indexArr = [];
            let _tempValue = {};
            let _finalData = {};
            _dataSource.forEach((item, index) => {
                let _sensorTypeId = _map[item.deviceType].find(sensor => sensor.name === item.sensorName)?.sensorTypeID || 0;
                let _type = _sensorType.find(sensor => sensor.id === _sensorTypeId)?.type ?? '';
                if (_type === '统计值') {
                    // 移除掉,并存储
                    _tempValue[`needToReplace_${item.stationCode}_${item.sensorName}`] = _dataSource.splice(index, 1, `needToReplace_${item.stationCode}_${item.sensorName}`)?.[0];
                    if (!_nameListMap[item.stationCode]) {
                        _nameListMap[item.stationCode] = {
                            code: item.stationCode,
                            deviceType: item.deviceType,
                            sensors: [item.sensorName]
                        }
                    } else {
                        _nameListMap[item.stationCode].sensors.push(item.sensorName)
356 357 358
                    }
                }
            })
359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392
            //6. 请求数据并替换数据。grid模式下,请求的时间是一致的。
            let baseParam = {
                pageIndex: 1,
                pageSize: 999,
                dateFrom: dateRange[0].dateFrom,
                dateTo: dateRange[0].dateTo,
            }
            let _arr = Object.values(_nameListMap)
            for await (let item of _arr) {
                let _params = {
                    ...baseParam,
                    accountName: item.deviceType,
                    deviceCode: item.code,
                    nameTypeList: item.sensors.map(sensor => ({
                        name: sensor,
                        type: 'Sub'
                    })),
                    /*                nameTypeList: ['今日用电量', '今日供水量'].map(sensor => ({
                                        name: sensor,
                                        type: 'Sub'
                                    })),*/
                    dateType: returnDateType(dateRange[0])
                };
                // 虚拟点需要查出实际点后,进行查找
                let _realSensors = {};
                let _realSensorsMap = {};
                // 统计类的如果是虚拟点,那么需要查出实际数据来源的点,查出映射关系
                (await getSensorsRealName(_params))?.data?.forEach(sensor => {
                    // name 虚拟点 staticName实际的点
                    _realSensors[sensor.staticName] = sensor.name;
                    _realSensorsMap[sensor.name] = sensor.staticName;
                });
                // 请求统计数据时,需要使用实际点去获取
                _params.nameTypeList.forEach(sensor => {
393 394 395
                    if (_realSensors[sensor.name]) {
                        sensor.name = _realSensors[sensor.name]
                    }
396 397 398
                });
                // 获取数据后,将原始数据中的dataModel这部分替换掉
                ((await getStatisticsInfo(_params))?.data?.list?.[0].dNameDataList ?? [])?.forEach(obj => {
399 400 401 402 403 404 405 406 407 408 409
                    if (_realSensorsMap[obj.dName]) {
                        let _v = _tempValue[`needToReplace_${item.code}_${_realSensorsMap[obj.dName]}`];
                        _v.dataModel = obj.nameDate.map(d => {
                            return {
                                pt: moment(d.time),
                                pv: d.value,
                                maxPV: d.value, minPV: d.value, firstPV: d.value, lastPV: d.value,
                            }
                        });
                        _finalData[`needToReplace_${item.code}_${_realSensorsMap[obj.dName]}`] = _v;
                    }
410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427
                });
                // 替换数据
                _dataSource.forEach((d, index) => {
                    if (_.isString(d) && d.includes('needToReplace') && _finalData[d]) {
                        _dataSource[index] = _finalData[d];
                    }
                })
                // 有不存在数据的,将原始数据替换回来
                _dataSource.forEach((d, index) => {
                    if (_.isString(d) && d.includes('needToReplace')) {
                        _dataSource[index] = dataSource[index];
                    }
                })
            }
            props.setLoading(false);
            return _dataSource
        } catch (e) {
            props.setLoading(false);
陈龙's avatar
陈龙 committed
428
            return []
429 430 431 432 433 434 435 436 437 438 439
        }
    };
    const returnDateType = (date) => {
        let {dateFrom, dateTo} = date;
        let _duration = moment.duration(moment(dateTo) - moment(dateFrom), 'ms').days();
        if (_duration >= 7) return 'month';
        if (_duration >= 30) return 'year';
        return 'day';
    };
    useEffect(() => {
        async function handle() {
陈龙's avatar
陈龙 committed
440 441
            let _data = isInit ? dataSource : (await handleDataSource(dataSource) ?? []);
            setIsInit(false);
442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459
            const grids = _data.reduce((pre, item, index) => {
                const {sensorName, deviceType} = item;
                const key = `${deviceType}_${sensorName}`; // 同设备类型同指标才在同一组
                let grid = pre.find((g) => g.key === key);
                if (!grid) {
                    const restProp = _.pick(item, ['equipmentName', 'sensorName', 'stationCode', 'unit']);
                    grid = {
                        key: key,
                        list: [],
                        ...restProp,
                    };
                    pre.push(grid);
                }
                grid.list.push(item);
                return pre;
            }, []);
            setGridData(grids);
        }
460

461 462
        handle();
    }, [dataSource])
463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491
    const options = useMemo(() => {
        let _options = gridData.map((item) => {
            const {key, list, equipmentName, sensorName, stationCode, unit} = item;
            let max = 300;
            // 5:左侧竖条的宽度;10:标题部分的左侧margin;
            // sensorName长度*单个宽度16.7;5:单位部分的左侧margin;
            // 91:单位部分的宽度(格式固定,宽度相对固定)
            let maxTitleLength = 5 + 10 + sensorName.length * 16.7 + 5 + 91;
            let finalLength = maxTitleLength > max ? max : maxTitleLength
            const cusOption = {
                title: {
                    show: true,
                    // text: `{prefix|}{t|${sensorName}}${unit ? '{suffix|(单位:' + unit + ')}' : ''}`,
                    text: ' ',
                    textStyle: {
                        width: finalLength,
                        overflow: 'truncate',
                    },
                },
                legend: {
                    // orient: 'vertical',
                    itemGap: 10,
                    padding: [0, 0, 0, finalLength],
                    textStyle: {
                        width: 120,
                        overflow: 'truncate',
                    },
                },
            };
492
            const option = optionGenerator(list, cusOption, null, contrastOption, smooth, {
493 494 495
                curveCenter,
                nameWithSensor: false,
                showGridLine: true,
496 497
                isMultiple: gridData.length > 1,
                chartType: 'lineChart'
498
            });
陈龙's avatar
陈龙 committed
499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521
            // 无数据时,图表需要显示默认图形 2024年3月14日
            // 1. x轴
            let dataEmpty = [];
            option.series.forEach(item => {
                if (item.data.length === 0) {
                    dataEmpty.push(true)
                    item.data = [[moment(dataSource?.[0]?.dateFrom).valueOf(), null], [moment(dataSource?.[0]?.dateTo).valueOf(), null]]
                } else {
                    dataEmpty.push(false);
                }
            })
            // 2. y轴
            let allEmpty = dataEmpty.length ? dataEmpty.reduce((final, cur) => {
                if (!cur) final = false;
                return final
            }, true) : true;
            if (allEmpty) {
                option.yAxis.forEach(item => {
                    item.max = 100;
                    item.min = 0;
                });
                option.tooltip = false;
            }
522 523
            delete option.xAxis.max
            delete option.xAxis.min
524 525 526 527 528 529 530
            return {
                key,
                option: option,
            };
        });
        return _options;
    }, [gridData, smooth, curveCenter]);
李纪文's avatar
李纪文 committed
531 532 533 534
    // const chartRef = dataSource.map((item, index) => useRef(null));
    // useEffect(() => {
    //     chartRef.forEach(item => {item?.current?.resize?.()})
    // }, [options]);
535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553
    return (
        <div className={`${prefixCls}-grid`}>
            {options.map((item, index) => {
                const {sensorName, unit} = gridData[index];
                const isEmpty =
                    !item.option.series.length ||
                    !item.option.series.find((e) => e.data && e.data.length > 0);
                return (
                    <div
                        key={item.key}
                        className={`${prefixCls}-grid-item`}
                        style={{
                            height: gridData.length === 1 ? '100%' : '',
                            width: gridData.length === 1 ? '100%' : '',
                        }}
                    >
                        <div className={`${prefixCls}-grid-item-wrap`}>
                            <ChartTitle prefixCls={prefixCls} title={sensorName} unit={unit}/>
                            {isEmpty ? (
554
                                isInit ? '' : <PandaEmpty/>
555
                            ) : (
556
                                <ChartWidthRef
557
                                    isMultiple={gridData.length > 1}
558
                                    data={gridData[index]}
559 560 561 562 563 564 565 566 567 568 569
                                    style={{width: '100%', height: '100%'}}
                                    option={item.option}
                                    notMerge
                                />
                            )}
                        </div>
                    </div>
                );
            })}
        </div>
    );
570 571
});

572
export default GridChart;