Commit e6ad7ff7 authored by 崔佳豪's avatar 崔佳豪

feat: 新增历史曲线业务组件

parent 4be9d1c4
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
**/node_modules
# roadhog-api-doc ignore
/src/utils/request-temp.js
_roadhog-api-doc
# production
**/dist
/.vscode
**/**/lib/**
**/**/es/**
# misc
.DS_Store
npm-debug.log*
yarn-error.log
/coverage
.idea
package-lock.json
*bak
.vscode
# visual studio code
.history
*.log
functions/mock
.temp/**
# umi
.umi
.umi-production
# screenshot
screenshot
.firebase
.eslintcache
.changelog
# devServer-host
host/
webpack.host.js
\ No newline at end of file
# `@wisdom-components/ec_historyview`
> TODO: description
## Usage
```
const ecHistoryview = require('@wisdom-components/ec_historyview');
// TODO: DEMONSTRATE API
```
{
"name": "@wisdom-components/ec_historyview",
"version": "1.0.0",
"description": "> TODO: description",
"author": "cuijiahao <15927252954@163.com>",
"homepage": "",
"license": "ISC",
"main": "src/index.js",
"directories": {
"lib": "lib",
"test": "__tests__"
},
"files": [
"lib"
],
"publishConfig": {
"registry": "https://g.civnet.cn:4873/"
},
"repository": {
"type": "git",
"url": "https://g.civnet.cn:8443/ReactWeb5/wisdom-components.git"
},
"scripts": {
"test": "echo \"Error: run tests from root\" && exit 1"
}
}
---
title: EC_HistoryView - 历史曲线
nav:
title: 业务组件
path: /extend-components
group:
path: /
---
# HistoryView 历史数据查看
基础业务组件
- 曲线模式
- 数据强制自动抽稀
- 表格模式
- 默认开启抽稀模式,可选择抽稀
## 何时使用
- 以图表或表格形式,查看历史数据时。
## 单图表
<code src="./demos/index.js">
## 多图表
<code src="./demos/GridDemo.js">
## API
| 参数 | 说明 | 类型 | 默认值 | 可选值 |
| --- | --- | --- | --- | --- |
| grid | 是否为分组模式 | boolean | false | - |
| title | 标题 | string | 指标曲线 | - |
| defaultChecked | 默认选中自定义时间 key | string | `oneHour` | `oneHour`/`fourHour`/`twelveHours`/`roundClock`/`yesterday` |
| tableProps | 表格其他 props | object | { } | - |
| deviceParams | 设备参数信息 | array | - | - |
### deviceParams
```javascript
[
{
deviceCode: 'EGBF00000146', // 设备编码
sensors: '进水压力,出水瞬时流量', // 设备查询指标
deviceCode: '二供泵房', // 设备类型
},
...
]
```
\ No newline at end of file
import React, { memo, useMemo } from 'react';
import _ from 'lodash';
import { BasicChart } from '@wisdom-components/basicchart';
import optionGenerator from './utils';
const GridChart = memo((props) => {
const { dataSource, contrast = false, contrastOption = 'day', smooth = true } = props;
const { prefixCls } = props;
const gridData = useMemo(() => {
const grids = dataSource.reduce((pre, item, index) => {
const { sensorName } = item;
let grid = pre.find((g) => g.key === sensorName);
if (!grid) {
const restProp = _.pick(item, ['equipmentName', 'sensorName', 'stationCode', 'unit']);
grid = {
key: sensorName,
list: [],
...restProp,
};
pre.push(grid);
}
grid.list.push(item);
return pre;
}, []);
return grids;
}, [dataSource]);
const options = useMemo(() => {
return gridData.map((item) => {
const { key, list, equipmentName, sensorName, stationCode, unit } = item;
const cusOption = {
title: {
show: true,
text: `{prefix|}{t|${sensorName}}${unit ? '{suffix|(单位:' + unit + ')}' : ''}`,
},
};
const option = optionGenerator(list, cusOption, contrast, contrastOption, smooth);
return {
key,
option: option,
};
});
}, [gridData, smooth]);
return (
<div className={`${prefixCls}-grid`}>
{
options.map(item => (
<div key={item.key} className={`${prefixCls}-grid-item`}>
<div className={`${prefixCls}-grid-item-wrap`}>
<BasicChart style={{ width: '100%', height: '100%' }} option={item.option} notMerge />
</div>
</div>
))
}
</div>
);
});
export default GridChart;
import React, { memo, useMemo } from 'react';
import { BasicChart } from '@wisdom-components/basicchart';
import optionGenerator from './utils';
const SimgleChart = memo((props) => {
const { dataSource, contrast = false, contrastOption = 'day', smooth = true } = props;
const option = useMemo(() => {
const config = {
needUnit: true,
};
return optionGenerator(dataSource, null, contrast, contrastOption, smooth, config);
}, [dataSource, smooth]);
return <BasicChart option={option} notMerge style={{ width: '100%', height: '100%' }}/>
});
export default SimgleChart;
\ No newline at end of file
import { request } from '@wisdom-utils/utils';
const REQUEST_METHOD_GET = 'get';
const REQUEST_METHOD_POST = 'post';
// eslint-disable-next-line no-undef
const baseUrl = typeof DUMI_TYPE !== 'undefined' && DUMI_TYPE === 'dumi' ? '/api' : '';
// 获取历史数据
export function getHistoryInfo(data) {
return request({
url: `${baseUrl}/PandaMonitor/Monitor/Device/GetSensorsDataForStation`,
method: REQUEST_METHOD_POST,
data,
});
}
import React from 'react';
import HistoryView from '../index';
const deviceParams = [{
deviceCode: "EGBF00000146",
sensors: "进水压力,出水瞬时流量,出水累计流量",
deviceType: "二供泵房"
}]
const Demo = () => {
return (
<div style={{height: 700}}>
<HistoryView deviceParams={deviceParams} grid />
</div>
);
};
export default Demo;
import React from 'react';
import HistoryView from '../index';
const deviceParams = [{
deviceCode: "EGBF00000146",
sensors: "进水压力,出水瞬时流量,出水累计流量",
deviceType: "二供泵房"
}]
const Demo = () => {
return (
<div style={{height: 700}}>
<HistoryView deviceParams={deviceParams} />
</div>
);
};
export default Demo;
const TIMELIST = [
{
key: 'oneHour',
name: '近1小时',
},
{
key: 'fourHour',
name: '近4小时',
},
{
key: 'twelveHours',
name: '近12小时',
},
{
key: 'roundClock',
name: '近24小时',
},
{
key: 'yesterday',
name: '昨天',
},
];
import React, { useContext, useEffect, useMemo, useState } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { Button, Checkbox, ConfigProvider, DatePicker, Radio, Select, Spin, Tabs } from 'antd';
import { CloseCircleFilled, DownloadOutlined, PlusCircleOutlined } from '@ant-design/icons';
import moment from 'moment';
import _ from 'lodash';
import TimeRangePicker from '@wisdom-components/timerangepicker';
import BasicTable from '@wisdom-components/basictable';
import { getHistoryInfo } from './apis';
import SimgleChart from './SingleChart';
import GridChart from './GridChart';
import './index.less';
const { RangePicker } = DatePicker;
const { Option } = Select;
const startFormat = 'YYYY-MM-DD 00:00:00';
const endFormat = 'YYYY-MM-DD 23:59:59';
const timeFormat = 'YYYY-MM-DD HH:mm:ss';
const timeList = [
{
key: 'oneHour',
name: '近1小时',
},
{
key: 'fourHour',
name: '近4小时',
},
{
key: 'twelveHours',
name: '近12小时',
},
{
key: 'roundClock',
name: '近24小时',
},
{
key: 'yesterday',
name: '昨天',
},
];
const CheckboxData = [
{
key: 'curveCenter',
label: '曲线居中',
checked: false,
},
{
key: 'ignoreOutliers',
label: '过滤异常值',
type: 'updateIgnoreOutliers',
checked: false,
},
{
key: 'dataThin',
label: '数据抽稀',
type: 'updateDataThin',
checked: true,
},
];
const timeIntervalList = [
{
key: '5',
zoom: '5',
unit: 'min',
name: '5分钟',
},
{
key: '10',
zoom: '10',
unit: 'min',
name: '10分钟',
},
{
key: '30',
zoom: '30',
unit: 'min',
name: '30分钟',
},
{
key: '1',
zoom: '1',
unit: 'h',
name: '1小时',
},
{
key: '2',
zoom: '2',
unit: 'h',
name: '2小时',
},
{
key: '6',
zoom: '6',
unit: 'h',
name: '6小时',
},
{
key: '12',
zoom: '12',
unit: 'h',
name: '12小时',
},
];
const updateTime = (key) => {
let start = '',
end = '';
if (Array.isArray(key)) {
start = moment(key[0]).format(timeFormat);
end = moment(key[1]).format(timeFormat);
} else {
switch (key) {
case 'oneHour':
start = moment().subtract(1, 'hour').format(timeFormat);
end = moment().format(timeFormat);
break;
case 'fourHour':
start = moment().subtract(4, 'hour').format(timeFormat);
end = moment().format(timeFormat);
break;
case 'twelveHours':
start = moment().subtract(12, 'hour').format(timeFormat);
end = moment().format(timeFormat);
break;
case 'roundClock':
start = moment().subtract(24, 'hour').format(timeFormat);
end = moment().format(timeFormat);
break;
case 'yesterday':
start = moment().subtract(1, 'days').format(startFormat);
end = moment().subtract(1, 'days').format(endFormat);
break;
}
}
return [
{
dateFrom: start,
dateTo: end,
},
];
};
const DefaultDatePicker = (value) => [
{
key: 1,
value: moment(),
},
{
key: 2,
value: moment().subtract(1, value),
},
];
const handleBatchTime = (arr, cOption) => {
let newArr = [];
arr.forEach((child) => {
if (child.value) {
newArr.push({
dateFrom: moment(child.value).startOf(cOption).format(startFormat),
dateTo: moment(child.value).endOf(cOption).format(endFormat),
});
}
});
newArr = _.uniqWith(newArr, _.isEqual); // 去掉重复日期时间
return newArr;
};
const timeColumn = {
title: '采集时间',
dataIndex: 'time',
key: 'time',
width: 160,
fixed: 'left',
ellipsis: true,
align: 'center',
};
const HistoryView = props => {
const { getPrefixCls } = useContext(ConfigProvider.ConfigContext);
const prefixCls = getPrefixCls('history-view');
const { title, grid, defaultChecked, tableProps, deviceParams } = props;
const [loading, setLoading] = useState(false);
const [activeTabKey, setActiveTabKey] = useState('curve');
// 时间模式: 自定义模式/同期对比模式
const [timeValue, setTimeValue] = useState('customer');
// 自定义模式
const [customerChecked, setCustomerChecked] = useState(defaultChecked); // 时间快速选择类型值
const [customerTime, setCustomerTime] = useState(); // 自定义时间选择值
// 同期对比模式
const [contrastOption, setContrastOption] = useState('day'); // 对比时间类型: 日/月
const [datePickerArr, setDatePickerArr] = useState(DefaultDatePicker('day')); // 对比时间段配置值
const [checkboxData, setCheckboxData] = useState(CheckboxData); // 曲线设置项
const [dataThinKey, setDataThinKey] = useState(timeIntervalList[0].key); // 曲线抽稀时间设置
const [columns, setColumns] = useState([]);
const [tableData, setTableData] = useState([]);
const [chartDataSource, setChartDataSource] = useState([]);
// 选择的时间范围值
const dateRange = useMemo(() => {
if (timeValue === 'customer') {
return updateTime(customerChecked || customerTime);
} else {
return handleBatchTime(datePickerArr, contrastOption);
}
}, [contrastOption, customerChecked, customerTime, datePickerArr, timeValue]);
// 数据配置
const dataConfig = useMemo(() => {
const initial = {
ignoreOutliers: false,
dataThin: false,
zoom: '', // 数据抽稀时间
unit: '', // 数据抽稀时间单位
};
// 曲线居中,过滤异常值,数据抽稀
const config = checkboxData.reduce((pre, item) => ((item.key !== 'curveCenter' && (pre[item.key] = item.checked)), pre), initial);
// 数据抽稀时间单位
const dataThin = timeIntervalList.find(item => item.key === dataThinKey);
config.zoom = activeTabKey === 'curve' ? '' : (dataThin?.zoom ?? '');
config.unit = activeTabKey === 'curve' ? '' : (dataThin?.unit ?? '');
config.dataThin = activeTabKey === 'curve' ? true : config.dataThin; // 曲线强制抽稀
return config;
}, [checkboxData, dataThinKey, activeTabKey]);
// 图表居中
const curveCenter = useMemo(() => checkboxData.find(item => item.key === 'curveCenter')?.checked, [checkboxData]);
// 自定义模式: 快速选择
const onCustomerTimeChange = (key) => {
setCustomerChecked(key);
!!customerTime && setCustomerTime(null);
};
// 自定义模式: 自定义时间选择
const onCustomerRangeChange = (value) => {
if (!value) {
// 时间清空,回到默认时间选择
setCustomerChecked('oneHour');
setCustomerTime(value);
} else {
setCustomerChecked(null);
setCustomerTime(value);
}
};
// 同期对比模式: 选择(日/月)
const onContrastChange = (value) => {
setContrastOption(value);
setDatePickerArr([...DefaultDatePicker(value)]);
};
// 同期对比模式: 时间段选择
const onContrastPickerChange = (date, dateString, item) => {
const arr = [...datePickerArr];
arr.forEach((child) => {
if (child.key === item.key) {
child.value = date;
}
});
setDatePickerArr(arr);
};
// 同期对比模式: 新增日期选择组件
const handleAddDatePicker = () => {
setDatePickerArr([
...datePickerArr,
{
key: datePickerArr[datePickerArr.length - 1].key + 1,
value: '',
},
]);
};
// 同期对比模式: 删除日期选择组件
const handleDeleteDatePicker = (index) => {
const arr = [...datePickerArr];
arr.splice(index, 1);
setDatePickerArr(arr);
};
// 时间设置切换(自定义/同期对比)
const onTimeSetChange = (e) => {
setTimeValue(e.target.value);
if (e.target.value === 'contrast') {
// 同期对比
onContrastChange(contrastOption);
} else {
// 自定义
// 不需要处理
}
};
const renderTimeOption = () => {
return (
<div className={classNames(`${prefixCls}-date`)}>
<div className={classNames(`${prefixCls}-label`)}>时间</div>
<Radio.Group value={timeValue} onChange={onTimeSetChange}>
<Radio.Button value="customer">自定义</Radio.Button>
<Radio.Button value="contrast">同期对比</Radio.Button>
</Radio.Group>
{timeValue === 'customer' && ( // 自定义
<>
<TimeRangePicker
onChange={onCustomerTimeChange}
value={customerChecked}
dataSource={timeList}
/>
<RangePicker
className={classNames(`${prefixCls}-custime-customer`)}
onChange={onCustomerRangeChange}
value={customerTime}
showTime
/>
</>
)}
{timeValue === 'contrast' && ( // 同期对比
<>
<Select value={contrastOption} style={{ width: 60 }} onChange={onContrastChange}>
<Option value="day"></Option>
<Option value="month"></Option>
</Select>
{datePickerArr.map((child, index) => (
<div key={child.key} className={classNames(`${prefixCls}-contrast-list`)}>
<div className={classNames(`${prefixCls}-contrast-wrap`)}>
<DatePicker
picker={contrastOption}
value={child.value}
onChange={(date, dateString) =>
onContrastPickerChange(date, dateString, child)
}
/>
{datePickerArr.length > 2 && (
<div
className={classNames(`${prefixCls}-contrast-delete`)}
onClick={() => handleDeleteDatePicker(index)}
>
<CloseCircleFilled />
</div>
)}
</div>
{index < datePickerArr.length - 1 && (
<div className={classNames(`${prefixCls}-contrast-connect`)}></div>
)}
</div>
))}
{datePickerArr.length < 5 && <PlusCircleOutlined onClick={handleAddDatePicker} />}
</>
)}
</div>
);
};
// 曲线设置项选择/取消
const onCheckboxChange = (e, key) => {
let data = [...checkboxData];
data.forEach((item) => {
if (item.key === key) {
item.checked = e.target.checked;
}
});
setCheckboxData(data);
};
// 数据抽稀时间间隔
const onTimeIntervalChange = (value) => {
setDataThinKey(value);
};
const renderCheckbox = (child) => (
<Checkbox
checked={child.checked}
onChange={(e) => onCheckboxChange(e, child.key)}
>
{child.label}
</Checkbox>
);
const renderCurveOption = () => {
return (
<div className={classNames(`${prefixCls}-cover`)}>
<div className={classNames(`${prefixCls}-label`)}>曲线设置</div>
{checkboxData.map((child) => (
<div key={child.key}>
{activeTabKey === 'curve' && child.key === 'curveCenter' && renderCheckbox(child)}
{activeTabKey === 'table' && child.key !== 'curveCenter' && renderCheckbox(child)}
</div>
))}
{activeTabKey === 'table' && (
<Select value={dataThinKey} style={{ width: 90 }} onChange={onTimeIntervalChange} disabled={!dataConfig.dataThin}>
{timeIntervalList.map((child) => (
<Option key={child.key} unit={child.unit} value={child.key}>
{child.name}
</Option>
))}
</Select>
)}
</div>
)
};
const exportExcelBtn = () => { };
const handleTableData = (data) => {
const ignoreOutliers = checkboxData.find((item) => item.key === 'ignoreOutliers').checked;
const dataIndexAccess = (dataItem, index) => {
const { stationCode, sensorName } = dataItem;
return `${stationCode}-${sensorName}-${index}`;
};
let format = timeFormat;
if (timeValue === 'contrast') {
format = contrastOption === 'day' ? '2020-01-01 HH:mm:00' : '2020-01-DD HH:mm:00';
}
// 处理表头数据
const columnsData = data.map((item, index) => {
const { stationCode, equipmentName, sensorName, unit, dataModel } = item;
const dataIndex = dataIndexAccess(item, index);
let col = {
title: `${equipmentName}-${sensorName}${unit ? `(${unit})` : ''}`,
dataIndex: dataIndex,
key: dataIndex,
ellipsis: true,
align: 'center',
};
// 同期对比
if (timeValue === 'contrast' && dataModel[0]) {
const time = item.dataModel[0].pt
.slice(0, contrastOption === 'day' ? 10 : 7)
.replace(/-/g, '');
col.title = `${equipmentName}-${sensorName}-${time}`;
}
return col;
});
// 格式化时间对齐数据, 生成行数
const timeData = {};
const buildDefaultData = (time) => {
const obj = { key: time, time: time };
data.forEach((item, index) => {
const dataIndex = dataIndexAccess(item, index);
obj[dataIndex] = '--';
});
return obj
}
data.forEach((item, index) => {
const { stationCode, sensorName, dataModel } = item;
dataModel.forEach((data) => {
const formatTime = moment(data.pt).format(format);
let time = formatTime;
if (timeValue === 'contrast') {
time = time.slice(contrastOption === 'day' ? 11 : 8, 16);
}
timeData[formatTime] = timeData[formatTime] || buildDefaultData(time);
});
});
// 处理表格数据
data.forEach((child, index) => {
const { dataModel } = child;
const dataIndex = dataIndexAccess(child, index);
dataModel.forEach((value, j) => {
const formatTime = moment(value.pt).format(format);
const dataRow = timeData[formatTime];
if (dataRow) {
dataRow[dataIndex] = value.pv === null ? '--' : value.pv;
}
});
});
const timeSort = (a, b) => {
let aa = a,
bb = b;
if (timeValue === 'contrast') {
aa = a.slice(contrastOption === 'day' ? 11 : 8, 16);
bb = b.slice(contrastOption === 'day' ? 11 : 8, 16);
}
return aa.localeCompare(bb);
};
const times = Object.keys(timeData).sort(timeSort);
const tableData = times.map((time) => timeData[time]);
setColumns([timeColumn, ...columnsData]);
setTableData(tableData);
};
// 处理接口服务参数的变化
const onChangeParams = (value = {}) => {
const { dateRange, isDilute, ignoreOutliers, zoom, unit } = value;
const requestArr = [];
dateRange.forEach((item) => {
const param = {
isDilute,
zoom,
unit,
ignoreOutliers,
isVertical: false, // 是否查询竖表
dateFrom: item.dateFrom,
dateTo: item.dateTo,
acrossTables: deviceParams,
};
requestArr.push(getHistoryInfo(param));
});
setLoading(true);
Promise.all(requestArr).then((results) => {
if (results.length) {
let data = [];
results.forEach((res, index) => {
const { dateFrom, dateTo } = dateRange?.[index] ?? {};
if (res.code === 0 && res.data.length) {
res.data.forEach(d => {
d.dateFrom = dateFrom;
d.dateTo = dateTo;
});
data = data.concat(res.data);
}
});
setLoading(false);
handleTableData(data);
setChartDataSource(data);
}
});
};
useEffect(() => {
const { dataThin, ignoreOutliers, zoom, unit } = dataConfig;
onChangeParams({
isDilute: dataThin,
ignoreOutliers,
zoom,
unit,
dateRange,
});
}, [dateRange, dataConfig, deviceParams]);
return (
<div className={classNames(prefixCls)}>
<Spin spinning={loading} wrapperClassName={classNames(`${prefixCls}-spin`)}>
<Tabs
activeKey={activeTabKey}
onChange={(key) => setActiveTabKey(key)}
centered
tabBarExtraContent={{
left: <h3>{title}</h3>,
right: (
<div className={`${prefixCls}-extra-right`}>
{/* {activeTabKey === 'table' && (
<Button onClick={exportExcelBtn}>
<DownloadOutlined />
下载
</Button>
)} */}
</div>
)
}}
>
<Tabs.TabPane key="curve" tab="曲线">
<div className={`${prefixCls}-options`}>
{renderTimeOption()}
{renderCurveOption()}
</div>
<div className={`${prefixCls}-content`}>
{
grid === true ? (
<GridChart prefixCls={prefixCls} dataSource={chartDataSource} contrast={timeValue === 'contrast'} contrastOption={contrastOption} />
) : (
<SimgleChart prefixCls={prefixCls} dataSource={chartDataSource} contrast={timeValue === 'contrast'} contrastOption={contrastOption} />
)
}
</div>
</Tabs.TabPane>
<Tabs.TabPane key="table" tab="表格">
<div className={`${prefixCls}-options`}>
{renderTimeOption()}
{renderCurveOption()}
</div>
<div className={`${prefixCls}-content`}>
{
<BasicTable
dataSource={tableData}
columns={columns}
{...tableProps}
pagination={{ showQuickJumper: true, showSizeChanger: true }}
onChange={() => {}}
/>
}
</div>
</Tabs.TabPane>
</Tabs>
</Spin>
</div>
);
};
HistoryView.propTypes = {
grid: PropTypes.bool,
title: PropTypes.string,
defaultChecked: PropTypes.oneOf(['oneHour', 'fourHour', 'twelveHours', 'roundClock', 'yesterday']),
tableProps: PropTypes.object,
deviceParams: PropTypes.arrayOf(PropTypes.objectOf({
deviceCode: PropTypes.string,
sensors: PropTypes.string,
deviceType: PropTypes.string,
}))
};
HistoryView.defaultProps = {
grid: false,
title: '指标曲线',
defaultChecked: 'oneHour',
tableProps: {},
};
export default HistoryView;
@root-entry-name: 'default';
@import '~antd/es/style/themes/index.less';
@history-view: ~'@{ant-prefix}-history-view';
.@{history-view} {
height: 100%;
&-label {
position: relative;
width: 80px;
&::after {
position: absolute;
top: 0;
right: 7px;
content: ':';
}
}
&-extra-right {
width: 82px;
}
.@{history-view}-spin,
.@{history-view}-spin > .@{ant-prefix}-spin-container,
.@{ant-prefix}-tabs,
.@{ant-prefix}-tabs-content,
.@{ant-prefix}-tabs-tabpane {
height: 100%;
}
.@{ant-prefix}-tabs-tabpane-active {
display: flex;
flex-direction: column;
}
}
.@{history-view}-date {
display: flex;
align-items: center;
white-space: nowrap;
.@{history-view}-label {
letter-spacing: 27px;
&::after {
right: -20px;
}
}
.@{ant-prefix}-radio-group,
.@{ant-prefix}-select {
margin-right: 16px;
}
.anticon-plus-circle {
margin-left: 10px;
color: @primary-color;
font-size: 16px;
cursor: pointer;
}
}
.@{history-view}-contrast {
&-wrap {
position: relative;
cursor: pointer;
}
&-list {
display: flex;
align-items: center;
}
&-delete {
position: absolute;
top: -12px;
right: -8px;
.anticon.anticon-close-circle {
color: #d9d9d9;
background: white;
}
&:hover {
.anticon.anticon-close-circle {
color: rgba(0, 0, 0, 0.45);
background: white;
}
}
}
&-connect {
margin: 0 10px;
}
}
.@{history-view}-options {
display: flex;
align-items: center;
flex-wrap: wrap;
column-gap: 20px;
row-gap: 10px;
padding-bottom: 10px;
}
.@{history-view}-cover {
display: flex;
align-items: center;
white-space: nowrap;
}
.@{history-view}-content {
flex: 1;
overflow: hidden;
}
.@{history-view}-grid {
height: 100%;
overflow-y: auto;
display: flex;
flex-wrap: wrap;
background-color: #F0F2F5;
padding: 4px;
&-item {
width: 50%;
padding: 4px;
}
&-item-wrap {
height: 350px;
background-color: #FFF;
border-radius: 4px;
}
}
\ No newline at end of file
import moment from 'moment';
import _ from 'lodash';
/**
* 轴宽度, 用于计算多轴显示时, 轴线偏移和绘图区域尺寸
*/
const axisWidth = 40;
/**
* 图表系列名称格式化
* @param {*} data
* @param {boolean} contrast 是否为同期对比
* @param {*} contrastOption 同期对比周期配置, day|month
* @returns
*/
const nameFormatter = (data, contrast, contrastOption) => {
const { equipmentName, sensorName, unit, dataModel, dateFrom, dateTo } = data;
let name = `${equipmentName}-${sensorName}`;
if (contrast) {
const time = dateFrom.slice(0, contrastOption === 'day' ? 10 : 7).replace(/-/g, '');
name = `${name}-${time}`;
}
return name;
};
/**
* 图表系列数据格式化
* @param {*} data
* @param {boolean} contrast 是否为同期对比
* @param {*} 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.map(item => {
const time = contrast ? moment(item.pt).format(formatStr) : item.pt;
return [moment(time).valueOf(), item.pv];
});
};
/**
* 面积图配置(目前默认曲线图)
* @param {*} data 数据项
* @returns null/areaStyle, 为null显示曲线图, 为areaStyle对象显示为面积图.
*/
const areaStyleFormatter = (data) => {
const { sensorName } = data;
return sensorName && sensorName.indexOf('流量') > -1 ? {} : null;
};
/**
* 图表配置项生成
* @param {*} dataSource 数据源
* @param {*} cusOption 自定义属性
* @param {*} contrast 是否为同期对比
* @param {*} contrastOption 同期对比周期配置, day|month
* @param {*} smooth ture/false, 曲线/折线
* @param {*} config 额外配置信息
*/
const optionGenerator = (dataSource, cusOption, contrast, contrastOption, smooth, config) => {
const needUnit = _.get(config, 'needUnit', false);
// 自定义属性
const restOption = _.pick(cusOption, ['title',]);
// 一种指标一个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'
},
}
yAxisMap.set(key, axis);
}
});
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,
};
// 根据"指标名称"分类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 }
})();
const series = dataSource.map(item => {
const { sensorName, unit } = item;
const name = nameFormatter(item, contrast, contrastOption);
const data = dataAccessor(item, contrast, contrastOption);
const type = 'line';
const areaStyle = areaStyleFormatter(item);
const yAxisIndex = yAxisInterator.get(sensorName);
return {
notMerge: true,
name,
type,
data,
areaStyle,
yAxisIndex,
smooth,
unit,
}
});
// 由于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));
const xAxis = { type: 'time', min, max };
const tooltipTimeFormat = !contrast ? 'YYYY-MM-DD HH:mm:ss' : contrastOption === 'day' ? 'HH:mm' : 'DD HH:mm';
const tooltip = { timeFormat: tooltipTimeFormat };
return {
yAxis,
grid,
xAxis,
series,
tooltip,
...restOption,
}
};
export default optionGenerator;
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