Commit 123dadab authored by 李纪文's avatar 李纪文

feat: 增加报警曲线设置

parent 599de965
......@@ -21,4 +21,13 @@ export function getStatisticsInfo(data) {
method: REQUEST_METHOD_POST,
data,
});
}
// 获取节假日、工作日
export function getDateList(params) {
return request({
url: `${baseURI}/PandaMonitor/Monitor/Alert/GetDateList`,
method: REQUEST_METHOD_GET,
params,
});
}
\ No newline at end of file
......@@ -6,8 +6,9 @@ const Demos = () => {
const [open, setOpen] = useState(false);
const [sensors, setSensors] = useState('出水瞬时流量');
const onOk = () => {
const onOk = (data) => {
setOpen(false);
console.log(data);
};
const onCancel = () => {
......
import React from 'react';
import React, { useState } from 'react';
import { PredictionCurve } from '../index';
import { Button } from 'antd';
const Demos = () => {
const [open, setOpen] = useState(false);
const [sensors, setSensors] = useState('出水瞬时流量');
const onOk = () => {
setOpen(false);
};
const onCancel = () => {
setOpen(false);
};
return (
<>
<PredictionCurve />
<Button
onClick={() => {
setOpen(true);
}}
>
打开预测曲线
</Button>
<PredictionCurve
open={open}
onOk={onOk}
onCancel={onCancel}
deviceCode={'EGBF00000120'}
deviceType={'二供泵房'}
sensors={sensors}
/>
</>
);
};
......
import React, { useContext, useEffect, useRef, useState } from 'react';
import { ConfigProvider, Radio, Slider, InputNumber, Input } from 'antd';
import classNames from 'classnames';
import moment from 'moment';
import { BasicChart } from '@wisdom-components/basicchart';
import { getHistoryInfo, getDateList } from '../../apis';
import { timeArr } from '../utils';
import './index.less';
const HistoryTrend = (props) => {
const { getPrefixCls } = useContext(ConfigProvider.ConfigContext);
const prefixCls = getPrefixCls('history-trend');
const { deviceCode, sensors, deviceType, changeSpin } = props;
const chartRef = useRef(null);
const [sensitive, setSensitive] = useState(10); // 敏感度
const [timeType, setTimeType] = useState('近7日'); // 时间
const [timeData, setTimeData] = useState([]); // 时间
const [sensorData, setSensorData] = useState([]); // 所有数据
const [ruleData, setRuleData] = useState([]); // 预测数据
const [options, setOptions] = useState({});
// 日期处理
const dateMethod = async () => {
const arr = new Array(7).fill('list');
let data = arr.map((item, index) => {
return moment()
.subtract(index + 1, 'day')
.format('YYYY-MM-DD');
});
switch (timeType) {
case '7工作日':
const dateList1 = await getDateList({
n: 7,
isHoliday: false,
date: moment().subtract(1, 'day').format('YYYY-MM-DD'),
});
setTimeData(dateList1?.data || []);
break;
case '7节假日':
const dateList2 = await getDateList({
n: 7,
isHoliday: false,
date: moment().subtract(1, 'day').format('YYYY-MM-DD'),
});
setTimeData(dateList2?.data || []);
break;
default:
setTimeData([data.join(',')]);
break;
}
};
// 获取历史数据
const getSensorsData = async () => {
changeSpin(true);
if (!timeData.length) return setOptions({});
const reqs = [];
timeData.forEach((item) => {
const list = item.split(',');
const params = {
isDilute: true,
zoom: '5',
unit: 'min',
dateFrom: moment(list[list.length - 1]).format('YYYY-MM-DD 00:00:00'),
dateTo: moment(list[0]).format('YYYY-MM-DD 23:59:59'),
acrossTables: [{ deviceCode: deviceCode, sensors: sensors, deviceType: deviceType }],
isBoxPlots: true,
ignoreOutliers: true,
};
const req = getHistoryInfo(params);
reqs.push(req);
});
Promise.all(reqs).then((results) => {
changeSpin(false);
let historyData = [];
results.forEach((result) => {
const _historyData = result?.data?.[0]?.dataModel || [];
historyData = historyData.concat([..._historyData]);
});
console.log(historyData);
setSensorData(() => {
dataMonthod(historyData);
return historyData;
});
});
};
// 数据处理
const dataMonthod = (data) => {
let timeDatas = [];
const chartData = [];
const strTime = moment().format('YYYY-MM-DD');
timeData.forEach((item) => {
const list = item.split(',');
timeDatas = timeDatas.concat([...list]);
});
timeDatas.forEach((item) => {
const seriesData = data.filter((list) => {
return list.pt.indexOf(item) > -1;
});
const series = {
type: 'line',
name: item,
smooth: true,
areaStyle: {},
data: seriesData.map((list) => {
const pv = new Date(moment(list['pt']).format(strTime + ' HH:mm:ss')).getTime();
return [pv, list['pv']];
}),
};
chartData.push(series);
});
renderChart(chartData);
};
// 渲染图表
const renderChart = (chartData) => {
const option = {
xAxis: {
type: 'time',
axisTick: {
alignWithLabel: true,
},
axisLabel: {
formatter: (value) => {
return moment(value).format('HH:mm:ss');
},
},
boundaryGap: false,
},
yAxis: {
type: 'value',
name: 'm',
position: 'left',
alignTicks: true,
axisLabel: {
formatter: '{value}',
},
},
tooltip: {
formatter: function (params) {
const title = moment(params[0].axisValue).format('HH:mm:ss');
let html = `<div style="border-bottom: 1px solid #F0F0F0;color: #808080;margin-bottom: 5px;padding-bottom: 5px;">${title}</div><div>`;
params.forEach((item) => {
html += `<span style="display: inline-block;margin: 0 7px 2px 0;border-radius: 5px;width: 5px;height: 5px;background: ${item.color};"></span>${item.seriesName}:<span style="color: ${item.color}">${item.data[1]}</span><br />`;
});
return html;
},
},
series: chartData,
};
setOptions(option);
};
// 限值处理
const limitMethod = () => {
const pvArr = sensorData.map((item) => {
return item.pv;
});
let max = Math.max(...pvArr);
let min = Math.min(...pvArr);
console.log(max, min);
const data = [
(min * (1 - sensitive / 100)).toFixed(2) * 1,
(min * (1 + sensitive / 100)).toFixed(2) * 1,
(max * (1 - sensitive / 100)).toFixed(2) * 1,
(max * (1 + sensitive / 100)).toFixed(2) * 1,
];
const color = ['#CB2D2D', '#0087F7'];
const name = ['低低限', '低限', '高限', '高高限'];
const mark = name.map((item, index) => {
return {
name: item,
yAxis: data[index],
lineStyle: {
color: color[index % 2],
type: 'dashed',
},
label: {
color: color[index % 2],
},
};
});
let option = { ...options };
option.series[0].markLine = {
data: mark,
};
setOptions(option);
setRuleData(data);
props.backData(data);
};
useEffect(() => {
getSensorsData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [timeData]);
useEffect(() => {
dateMethod();
}, [timeType]);
useEffect(() => {
if (options?.series?.length && sensorData.length) limitMethod();
}, [sensitive, sensorData]);
return (
<div className={classNames(`${prefixCls}`)}>
<div className={classNames(`${prefixCls}-header`)}>
<div className={classNames(`${prefixCls}-list`)}>
<span>参考依据:</span>
<Radio.Group
options={timeArr}
optionType={'button'}
value={timeType}
onChange={(e) => {
setTimeType(e.target.value);
}}
/>
</div>
<div className={classNames(`${prefixCls}-list`)}>
<span>建议取值:</span>
<div className={classNames(`${prefixCls}-read`)}>
<div className={classNames(`${prefixCls}-value`)}>
<Input
style={{
width: '150px',
}}
value={ruleData[0]}
addonBefore="低低限"
disabled
/>
</div>
<div className={classNames(`${prefixCls}-value`)}>
<Input
style={{
width: '150px',
}}
value={ruleData[1]}
addonBefore="低限"
disabled
/>
</div>
<div className={classNames(`${prefixCls}-value`)}>
<Input
style={{
width: '150px',
}}
value={ruleData[2]}
addonBefore="高限"
disabled
/>
</div>
<div className={classNames(`${prefixCls}-value`)}>
<Input
style={{
width: '150px',
}}
value={ruleData[3]}
addonBefore="高高限"
disabled
/>
</div>
</div>
<div className={classNames(`${prefixCls}-range`)}>
允许浮动范围:
<Slider
min={0}
max={100}
style={{ width: '100px' }}
onChange={(value) => {
setSensitive(value);
}}
value={typeof sensitive === 'number' ? sensitive : 0}
/>
<InputNumber
min={1}
max={100}
style={{
margin: '0 16px',
width: '100px',
}}
formatter={(value) => `${value}%`}
value={sensitive}
onChange={(value) => {
setSensitive(value);
}}
/>
</div>
</div>
</div>
<div className={classNames(`${prefixCls}-chart`)}>
<BasicChart
ref={chartRef}
option={options}
notMerge
style={{ width: '100%', height: '100%' }}
/>
</div>
</div>
);
};
export default HistoryTrend;
@root-entry-name: 'default';
@import '~antd/es/style/themes/index.less';
@tree-custom-prefix-cls: ~'@{ant-prefix}-history-trend';
.@{tree-custom-prefix-cls} {
height: 100%;
display: flex;
flex-direction: column;
&-header {
flex: none;
}
&-list {
display: flex;
margin-bottom: 10px;
align-items: center;
}
&-read {
display: flex;
align-items: center;
}
&-range {
display: flex;
align-items: center;
}
&-value {
margin-right: 10px;
.@{ant-prefix}-input[disabled] {
background-color: #ffffff;
}
}
&-chart {
flex: 1;
overflow: hidden;
}
}
\ No newline at end of file
......@@ -3,10 +3,6 @@
@tree-custom-prefix-cls: ~'@{ant-prefix}-limit-curve';
.@{tree-custom-prefix-cls} {
.@{ant-prefix}-modal-body {
height: 650px;
}
&-box {
width: 100%;
height: 100%;
......@@ -15,38 +11,42 @@
position: relative;
}
&-header {
flex: none;
&-title {
display: flex;
width: 100%;
height: 100%;
position: relative;
&-list {
display: flex;
align-items: center;
width: 100%;
&-name {
position: absolute;
left: 0;
font-size: 16px;
}
&:last-of-type {
margin-top: 10px;
}
&-operate {
position: absolute;
right: 0;
}
&-item {
margin-right: 16px;
display: flex;
align-items: center;
justify-content: center;
.@{ant-prefix}-tabs {
width: 100%;
}
&-value {
margin-right: 10px;
.@{ant-prefix}-tabs-content {
height: 100%;
}
.@{ant-prefix}-input[disabled] {
background-color: #ffffff;
}
.@{ant-prefix}-btn {
margin-left: 10px;
}
}
&-content {
flex: 1;
overflow: hidden;
.@{ant-prefix}-modal-body {
height: 650px;
}
.@{ant-prefix}-tabs-tabpane-active {
height: 100%;
}
&-load {
......
import React, { useContext, useEffect, useRef, useState } from 'react';
import { ConfigProvider, Modal, Radio, Slider, InputNumber, Input, Button } 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 } 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 [sensitive, setSensitive] = useState(10); // 敏感度
const [timeCycle, setTimeCycle] = useState(60);
const [timeDan, setTimeDan] = useState(1);
const [options, setOptions] = useState({});
const chartRef = useRef(null);
// 确定
const onOk = () => {
props.onOk && props.onOk(123);
};
// 取消
const onCancel = () => {
setOpen(false);
props.onCancel && props.onCancel();
};
// 获取历史数据
const getSensorsData = async () => {
changeSpin(true);
const params = {
isDilute: true,
zoom: '',
unit: '',
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,
};
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 = () => {};
// 渲染图表
const renderChart = (_chartData) => {
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 option = {
xAxis: {
type: 'time',
axisTick: {
alignWithLabel: true,
},
boundaryGap: false,
splitLine: {
show: true,
lineStyle: {
type: 'dashed',
},
},
},
yAxis: {
type: 'value',
name: 'm',
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: sensors,
sampling: 'average',
large: true,
data: _centroids.map((item) => {
return [Math.floor(item[0]), item[1]];
}),
},
],
};
setOptions(option);
};
const proposeRender = () => {
return (
<>
<div className={classNames(`${prefixCls}-propose-list`)}>
<div className={classNames(`${prefixCls}-propose-value`)}>
<div className={classNames(`${prefixCls}-value-list`)}>
<Input
style={{
width: '150px',
}}
addonBefore="低低限"
disabled
/>
</div>
<div className={classNames(`${prefixCls}-value-list`)}>
<Input
style={{
width: '150px',
}}
addonBefore="低限"
disabled
/>
</div>
<div className={classNames(`${prefixCls}-value-list`)}>
<Input
style={{
width: '150px',
}}
addonBefore="高限"
disabled
/>
</div>
<div className={classNames(`${prefixCls}-value-list`)}>
<Input
style={{
width: '150px',
}}
addonBefore="高高限"
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) => {
setSensitive(value);
}}
value={typeof sensitive === 'number' ? sensitive : 0}
/>
<InputNumber
min={1}
max={100}
style={{
margin: '0 16px',
width: '100px',
}}
formatter={(value) => `${value}%`}
value={sensitive}
onChange={(value) => {
setSensitive(value);
}}
/>
</div>
</div>
</>
);
};
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;
});
const clusteredArr = [];
const dataArr = [];
const pvArr = data.map((item) => {
return item.pv;
});
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);
clusteredArr.push([new Date(item.time).getTime(), item.pv]);
});
const clustered = clusteredArr.length ? skmeans(clusteredArr, 1) : {};
const { centroids = [] } = clustered;
_chartData.push(...dataArr);
_clustered.push(...centroids);
}
renderChart(_chartData, _clustered);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sensorData, outlier]);
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) => {
console.log(outlierArr);
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;
@root-entry-name: 'default';
@import '~antd/es/style/themes/index.less';
@tree-custom-prefix-cls: ~'@{ant-prefix}-intellect-draw';
.@{tree-custom-prefix-cls} {
height: 100%;
display: flex;
flex-direction: column;
&-header {
flex: none;
}
&-list {
display: flex;
margin-bottom: 10px;
align-items: center;
}
&-label {
display: flex;
height: 32px;
align-items: center;
}
&-item {
display: flex;
align-items: center;
margin-right: 20px;
}
.@{ant-prefix}-modal-body {
height: 650px;
}
&-propose {
display: flex;
}
&-propose-list {
display: flex;
align-items: center;
}
&-value-list {
margin-right: 10px;
.@{ant-prefix}-input[disabled] {
background-color: #ffffff;
}
}
&-propose-value {
display: flex;
align-items: center;
}
&-propose-range {
display: flex;
align-items: center;
}
&-chart {
flex: 1;
overflow: hidden;
}
}
\ No newline at end of file
export const outlierArr = [
{
label: '关',
value: 0,
},
{
label: '低',
value: 3,
......@@ -13,17 +17,24 @@ export const outlierArr = [
},
];
export const markArr = {
0: '关',
1: '低',
2: '中',
3: '高',
};
export const timeArr = [
{
label: '近7',
value: '近7',
label: '近7日特征曲线',
value: '近7',
},
{
label: '7工作日',
label: '7工作日特征曲线',
value: '7工作日',
},
{
label: '7节假日',
label: '7节假日特征曲线',
value: '7节假日',
},
];
......@@ -42,4 +53,4 @@ export const chartArr = [
// 平均值方法
export const average = (arr) => {
return arr.reduce((acc, cur) => acc + cur, 0) / arr.length;
};
\ No newline at end of file
};
import React, { useContext } from 'react';
import { ConfigProvider } from 'antd';
import React, { useContext, useEffect, useRef, useState } from 'react';
import {
ConfigProvider,
Modal,
Radio,
Slider,
InputNumber,
Input,
Button,
Tabs,
Select,
} from 'antd';
import classNames from 'classnames';
import LoadBox from '@wisdom-components/loadbox';
import './index.less';
const PredictionCurve = () => {
const PredictionCurve = (props) => {
const { getPrefixCls } = useContext(ConfigProvider.ConfigContext);
const prefixCls = getPrefixCls('prediction-curve');
const { width, deviceCode, sensors, deviceType, getContainer, title } = props;
const [open, setOpen] = useState(false);
const [spinning, setSpinning] = useState(false);
const [sensitive, setSensitive] = useState(10); // 波动幅度
// 确定
const onOk = () => {
props.onOk && props.onOk(123);
};
// 取消
const onCancel = () => {
setOpen(false);
props.onCancel && props.onCancel();
};
// spin控制
const changeSpin = (flag) => {
setSpinning(flag);
};
const renderTitle = () => {
return (
<>
<div className={classNames(`${prefixCls}-title`)}>
<span className={classNames(`${prefixCls}-title-name`)}>预测算法预览</span>
<div className={classNames(`${prefixCls}-title-operate`)}>
<Button onClick={onCancel}>取消</Button>
<Button type={'primary'} onClick={onOk}>
确定
</Button>
</div>
</div>
</>
);
};
useEffect(() => {}, [open]);
useEffect(() => {
setOpen(props.open);
}, [props.open]);
return (
<>
<div className={classNames(`${prefixCls}`)}>预测报警</div>
<Modal
closable={false}
centered
width={width || '1200px'}
footer={null}
open={open}
onOk={onOk}
onCancel={onCancel}
wrapClassName={classNames(`${prefixCls}`)}
getContainer={getContainer || document.body}
destroyOnClose={true}
>
<div className={classNames(`${prefixCls}-box`)}>
{renderTitle()}
<div className={classNames(`${prefixCls}-info`)}>
<div className={classNames(`${prefixCls}-list`)}>
<span>预测算法:</span>
<Select
defaultValue="滑动平均值"
style={{
width: 120,
}}
onChange={() => {}}
options={[
{
value: '滑动平均值',
label: '滑动平均值',
},
]}
/>
</div>
<div className={classNames(`${prefixCls}-list`)}>
<span>预测模型:</span>
<Select
defaultValue="M1"
style={{
width: 120,
}}
onChange={() => {}}
options={[
{
value: 'M1',
label: 'M1',
},
]}
/>
</div>
<div className={classNames(`${prefixCls}-list`)}>
<span>预测准确度(参考)</span>
<span>{97}%</span>
</div>
</div>
<div className={classNames(`${prefixCls}-info`)}>
允许波动幅度:
<Slider
min={0}
max={100}
style={{ width: '100px' }}
onChange={(value) => {
setSensitive(value);
}}
value={typeof sensitive === 'number' ? sensitive : 0}
/>
<InputNumber
min={1}
max={100}
style={{
margin: '0 16px',
width: '100px',
}}
formatter={(value) => `${value}%`}
value={sensitive}
onChange={(value) => {
setSensitive(value);
}}
/>
</div>
<div className={classNames(`${prefixCls}-chart`)}></div>
{spinning && (
<div className={classNames(`${prefixCls}-load`)}>
<LoadBox spinning={spinning} />
</div>
)}
</div>
</Modal>
</>
);
};
......
......@@ -3,5 +3,63 @@
@tree-custom-prefix-cls: ~'@{ant-prefix}-prediction-curve';
.@{tree-custom-prefix-cls} {
background: red;
&-box {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
position: relative;
}
.@{ant-prefix}-modal-body {
height: 650px;
}
&-title {
display: flex;
width: 100%;
flex: none;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #f0f0f0;
padding-bottom: 10px;
margin-bottom: 20px;
&-name {
font-size: 16px;
}
.@{ant-prefix}-btn {
margin-left: 10px;
}
}
&-info {
display: flex;
align-items: center;
margin-bottom: 10px;
}
&-list {
margin-right: 20px;
}
&-chart {
flex: 1;
width: 100%;
overflow: hidden;
}
&-load {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(255, 255, 255, 0.9);
z-index: 100;
}
}
\ No newline at end of file
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