import moment from 'moment'; import _, {isArray} from 'lodash'; /** 轴宽度, 用于计算多轴显示时, 轴线偏移和绘图区域尺寸 */ const axisWidth = 40; const COLOR = { NORMAL: '#1685FF', UPER: '#fa8c16', UPUPER: '#FF0000', // LOWER: '#13c2c2', // LOWLOWER: '#2f54eb', LOWER: '#fa8c16', LOWLOWER: '#FF0000', AVG: '#00B8B1', }; /** * 图表系列名称格式化 * * @param {any} data * @param {boolean} contrast 是否为同期对比 * @param {any} contrastOption 同期对比周期配置, day|month * @returns */ const nameFormatter = (data, contrast, contrastOption, nameWithSensor) => { const {equipmentName, sensorName, unit, dataModel, dateFrom, dateTo} = data; let name = nameWithSensor ? `${equipmentName}-${sensorName}` : equipmentName; if (contrast) { const time = dateFrom.slice(0, contrastOption === 'day' ? 10 : 7).replace(/-/g, ''); name = `${name}-${time}`; } return name; }; /** * 图表系列数据格式化 * * @param {any} data * @param {boolean} contrast 是否为同期对比 * @param {any} contrastOption 同期对比周期配置, day|month * @returns 图表系列数据, [[DateTime, value]] */ const dataAccessor = (data, contrast, contrastOption) => { const {dataModel} = data; const formatStr = contrastOption === 'day' ? '2020-01-01 HH:mm:00' : '2020-01-DD HH:mm:00'; return dataModel.filter(item => item.sensorName !== '是否在线').map((item) => { const time = contrast ? moment(item.pt).format(formatStr) : item.pt; return [moment(time).valueOf(), item.pv]; }); }; /** * 面积图配置(目前默认曲线图) * * @param {any} data 数据项 * @returns Null/areaStyle, 为null显示曲线图, 为areaStyle对象显示为面积图. */ const areaStyleFormatter = (data) => { const {sensorName} = data; return sensorName && sensorName.indexOf('流量') > -1 ? {} : null; }; /** * 数据项中指标值最小最大值 * * @param {any} data 数据项 * @returns */ const minMax = (data) => { const {dataModel} = data; let min = Number.MAX_SAFE_INTEGER; let max = Number.MIN_SAFE_INTEGER; dataModel.forEach((item) => { min = Math.min(min, item.pv ?? 0); max = Math.max(max, item.pv ?? 0); }); return [min, max]; }; const markLineItem = (name, value, color) => { return { name: name, yAxis: value, value: value, lineStyle: { color: color || '#000', }, label: { position: 'insideEndTop', color: color || '#000', formatter: function () { return `${name}:${value}`; }, }, }; }; export const alarmMarkLine = (dataItem, index, dataSource, schemes) => { // 只有一个数据曲线时显示markline if (!dataItem || !schemes || dataSource.length !== 1) return {}; const {deviceType, stationCode, sensorName, decimalPoint} = dataItem; const curSchemes = schemes.filter( (item) => item.deviceCode === stationCode && item.sensorName === sensorName && item.valueType === '直接取值', ); const data = []; curSchemes.forEach((scheme) => { const {hLimit, hhLimit, lLimit, llLimit} = scheme; lLimit !== null && lLimit !== void 0 && data.push(markLineItem('低限', lLimit, '#fa8c16')); hLimit !== null && hLimit !== void 0 && data.push(markLineItem('高限', hLimit, '#fa8c16')); llLimit !== null && llLimit !== void 0 && data.push(markLineItem('低低限', llLimit, '#FF0000')); hhLimit !== null && hhLimit !== void 0 && data.push(markLineItem('高高限', hhLimit, '#FF0000')); }); data.push({ name: '平均线', type: 'average', lineStyle: { color: '#00b8b1', type: 'solid', }, label: { position: 'insideEndTop', color: '#00b8b1', formatter: function (param) { return `平均值:${param.value}`; }, }, }); return { symbol: ['none', 'none'], data, }; }; export const minMaxMarkPoint = (dataItem, index, dataSource) => { // 只有一个数据曲线时显示markline if (!dataItem || dataSource.length !== 1) return {}; const data = []; data.push({type: 'min', name: '最小: '}); data.push({type: 'max', name: '最大: '}); return { symbolSize: 1, symbolOffset: [0, '50%'], label: { formatter: '{b|{b} }{c|{c}}', backgroundColor: window.globalConfig && window.globalConfig && window.globalConfig.variableTheme?.primaryColor ? window.globalConfig.variableTheme.primaryColor : '#0087F7', borderColor: '#ccc', borderWidth: 1, borderRadius: 4, padding: [2, 10], lineHeight: 22, position: 'top', distance: 10, rich: { b: { color: '#fff', }, c: { color: '#fff', fontSize: 16, fontWeight: 700, }, }, }, data, }; }; /** * 坐标轴添加网格线配置 * * @param {any} axis */ export const decorateAxisGridLine = (axis, showGrid) => { if (!axis) return; axis.minorTick = { lineStyle: { color: '#e2e2e2', }, ...(axis.minorTick || {}), show: showGrid, splitNumber: 2, }; axis.minorSplitLine = { lineStyle: { color: '#e2e2e2', type: 'dashed', }, ...(axis.minorSplitLine || {}), show: showGrid, }; axis.splitLine = { ...(axis.splitLine || {}), show: showGrid, }; }; /** * 坐标轴添加离线区间 * * @param {any} dataItem */ export const offlineArea = (dataItem) => { if (!dataItem) return {}; const {dataModel} = dataItem; let datas = new Array(); let offlineData = []; let hasOffline = false; dataModel.forEach((item, index) => { if (!item.pv && !hasOffline) { offlineData = [ { name: '离线', xAxis: new Date(item.pt), }, ]; hasOffline = true; } else if ((item.pv && hasOffline)) { offlineData.push({ xAxis: new Date(item.pt), }); datas.push(offlineData); offlineData = []; hasOffline = false; } }); return { itemStyle: { color: '#eee', }, data: datas, }; }; /** * 图表配置项生成 * * @param {any} dataSource 数据源 * @param {any} cusOption 自定义属性 * @param {any} contrast 是否为同期对比 * @param {any} contrastOption 同期对比周期配置, day|month * @param {any} smooth Ture/false, 曲线/折线 * @param {any} config 额外配置信息 */ const optionGenerator = (dataSource, cusOption, contrast, contrastOption, smooth, config) => { const needUnit = _.get(config, 'needUnit', false); const curveCenter = _.get(config, 'curveCenter', false); const nameWithSensor = _.get(config, 'nameWithSensor', true); const showGridLine = _.get(config, 'showGridLine', false); const showMarkLine = _.get(config, 'showMarkLine', false); const showPoint = _.get(config, 'showPoint', false); const deviceAlarmSchemes = _.get(config, 'deviceAlarmSchemes', []); const chartType = _.get(config, 'chartType', null); const justLine = _.get(config, 'justLine', false); // 自定义属性 const restOption = _.pick(cusOption, ['title', 'legend']); // 一种指标一个y轴 const yAxisMap = new Map(); dataSource.forEach((item, index) => { const {sensorName, unit} = item; const key = sensorName; if (!yAxisMap.has(key)) { const i = yAxisMap.size; const axis = { type: 'value', name: needUnit ? unit : null, position: i % 2 === 0 ? 'left' : 'right', offset: Math.floor(i / 2) * axisWidth, axisLabel: { formatter: (value) => (value > 100000 ? `${value / 1000}k` : value), }, axisLine: { show: true, }, nameTextStyle: { align: i % 2 === 0 ? 'right' : 'left', }, minorTick: { lineStyle: { color: '#e2e2e2', }, }, minorSplitLine: { lineStyle: { color: '#e2e2e2', type: 'dashed', }, }, }; yAxisMap.set(key, axis); } // 曲线居中 if (curveCenter && item.dataModel && item.dataModel.length > 0) { const [min, max] = minMax(item); const axis = yAxisMap.get(key); axis.min = axis.min === void 0 ? min : Math.min(min, axis.min); axis.max = axis.max === void 0 ? max : Math.max(max, axis.max); } // 网格显示 const axis = yAxisMap.get(key); decorateAxisGridLine(axis, showGridLine); }); const yAxis = yAxisMap.size > 0 ? [...yAxisMap.values()] : {type: 'value'}; // 根据y轴个数调整边距 const leftNum = Math.ceil(yAxisMap.size / 2); const rightNum = Math.floor(yAxisMap.size / 2); const grid = { top: needUnit ? 80 : 60, left: 10 + leftNum * axisWidth, right: rightNum === 0 ? 20 : rightNum * axisWidth, }; const headTemplate = (param) => { if (!param) return ''; const {name, axisValueLabel, axisType, axisValue} = param; const timeFormat = 'YYYY-MM-DD HH:mm:ss'; const text = axisType === 'xAxis.time' ? moment(axisValue).format(timeFormat) : name || axisValueLabel; return `<div style="border-bottom: 1px solid #F0F0F0; color: #808080; margin-bottom: 5px; padding-bottom: 5px;">${text}</div>`; }; const seriesTemplate = (param, unit) => { if (!param) return ''; const {value, encode} = param; // const val = value[encode.y[0]]; const _unit = unit || 'Mpa'; const color = '#008CFF'; if (!isArray(value)) return ` <div style="display: flex; align-items: center;"> <span>${param.seriesName}</span><span style="display:inline-block;">:</span> <span style="color:${color};margin: 0 5px 0 auto;">${value?.toFixed(3) ?? '-'}</span> <span style="font-size: 12px;">${_unit}</span> </div>`; return ` <div style="display: flex; align-items: center;"> <span>首值</span><span style="display:inline-block;">:</span> <span style="color: ${COLOR.AVG};margin: 0 5px 0 auto;">${value[1] ?? '-'}</span> <span style="font-size: 12px;">${_unit}</span> </div> <div style="display: flex; align-items: center;"> <span>尾值</span><span style="display:inline-block;">:</span> <span style="color: ${COLOR.AVG};margin: 0 5px 0 auto;">${value[2] ?? '-'}</span> <span style="font-size: 12px;">${_unit}</span> </div> <div style="display: flex; align-items: center;"> <span>最小值</span><span style="display:inline-block;">:</span> <span style="color: ${COLOR.AVG};margin: 0 5px 0 auto;">${value[3] ?? '-'}</span> <span style="font-size: 12px;">${_unit}</span> </div> <div style="display: flex; align-items: center;"> <span>最大值</span><span style="display:inline-block;">:</span> <span style="color: ${COLOR.AVG};margin: 0 5px 0 auto;">${value[4] ?? '-'}</span> <span style="font-size: 12px;">${_unit}</span> </div> `; }; const tooltipAccessor = (unit) => { return { formatter: function (params, ticket, callback) { let tooltipHeader = ''; let tooltipContent = ''; if (isArray(params)) { tooltipHeader = headTemplate(params[0]); params.forEach((param) => { tooltipContent += seriesTemplate(param, unit); }); } else { tooltipHeader = headTemplate(params); tooltipContent += seriesTemplate(params, unit); } return ` <div> ${tooltipHeader} <div>${tooltipContent}</div> </div> `; } } }; // 根据"指标名称"分类yAxis const yAxisInterator = (() => { const map = new Map(); let current = -1; const get = (name) => (map.has(name) ? map.get(name) : map.set(name, ++current).get(name)); return {get}; })(); let _offlineData = []; let series = dataSource.filter(item => { if (item.sensorName === '是否在线') { _offlineData.push(item); } return item.sensorName !== '是否在线'; }).map((item, index) => { const {sensorName, unit} = item; const name = nameFormatter(item, contrast, contrastOption, nameWithSensor); const data = dataAccessor(item, contrast, contrastOption); const type = 'line'; const areaStyle = areaStyleFormatter(item); const yAxisIndex = yAxisInterator.get(sensorName); const markLine = showMarkLine ? alarmMarkLine(item, index, dataSource, deviceAlarmSchemes) : {}; const markPoint = showPoint ? minMaxMarkPoint(item, index, dataSource) : {}; let markArea = null; // 需求变更:设备离线改用“是否在线”的数据,离线的状态标记的数据用该部分的数据。 2023年4月25日09:36:55 let _offlineAreasData = _offlineData.map(item => { let _item = {...item}; _item.dataModel = item.dataModel.map(d => { let _d = {...d}; _d.pv = 0; return _d }) return _item }).find(offline => offline.stationCode === item.stationCode) let offlineAreas = offlineArea(_offlineAreasData); if (offlineAreas.data?.length) { restOption.markArea = null; markArea = offlineAreas; } return { notMerge: true, name, type, data, areaStyle, yAxisIndex, smooth, unit, markLine, markPoint, markArea, }; }); // 由于series更新后,没有的数据曲线仍然停留在图表区上,导致图表可视区范围有问题 const min = Math.min( ...series.map((item) => item.data?.[0]?.[0]).filter((item) => item !== undefined), ); const max = Math.max( ...series .map((item) => item.data?.[item.data.length - 1]?.[0]) .filter((item) => item !== undefined), ); let xAxis = {type: 'time', min, max}; decorateAxisGridLine(xAxis, showGridLine); const tooltipTimeFormat = !contrast ? 'YYYY-MM-DD HH:mm:ss' : contrastOption === 'day' ? 'HH:mm' : 'DD HH:mm'; let tooltip = { timeFormat: tooltipTimeFormat, // trigger: 'axis', // axisPointer: { // type: 'cross' // } }; // 增加箱线图的逻辑,单曲线才存在 if (!justLine && chartType) { if (chartType === 'boxChart') { const otherData = dataSource?.[0]?.dataModel.map(item => { const {firstPV, lastPV, maxPV, minPV} = item; return [firstPV, lastPV, minPV, maxPV] }) || []; //当存在othersData的时候,只是单曲线 xAxis = {type: 'category', data: series[0].data.map(item => moment(item[0]).format('YYYY-MM-DD'))}; let unit = '' series = series.map(item => { if (item.unit) unit = item.unit; let _item = {...item, symbol: 'none'}; _item.data = _item?.data?.map(d => { return d[1] || null }) || []; return _item; }) series.push({ type: 'candlestick', name: '箱线图', symbol: 'none', data: otherData, itemStyle: { color: '#FFA200', color0: '#44CD00', borderColor: '#FFA200', borderColor0: '#44CD00' } }); tooltip = tooltipAccessor(unit) } else { let _maxData = []; let _minData = []; dataSource?.[0]?.dataModel.forEach(item => { const {firstPV, lastPV, maxPV, minPV} = item; _maxData.push(maxPV); _minData.push(minPV); }); //当存在othersData的时候,只是单曲线 xAxis = {type: 'category', data: series[0].data.map(item => moment(item[0]).format('YYYY-MM-DD'))}; series = series.map(item => { let _item = {...item, symbol: 'none'}; _item.data = _item?.data?.map(d => { return d[1] || null }) || []; return _item; }); console.log(series); [[..._minData], [..._maxData]].forEach((item, index) => { series.push({ name: index === 0 ? '最小值' : '最大值', type: 'line', data: item, lineStyle: { opacity: 0 }, ...(index !== 0 ? { areaStyle: { color: series?.[0]?.itemStyle?.color ?? '#65a0d1', opacity: 0.2, } } : {}), stack: 'confidence-band', symbol: 'none' }); }) } } return { yAxis, grid, xAxis, series, tooltip, ...restOption, }; }; export default optionGenerator;