monitoring screen redesign

This commit is contained in:
Фролов 2021-04-16 15:50:01 +05:00
parent b1d73e33b9
commit 10b6dcc307
11 changed files with 399 additions and 132 deletions

View File

@ -0,0 +1,52 @@
import {ValueDisplay} from './Display'
import {ControlOutlined} from '@ant-design/icons'
import {Popover} from 'antd'
export const ChartTimeOnlineFooter = (props) =>{
const { data,
lineIdle,
lineSp,
linesOther} = props
let sp = null
let idle = null
if(data && lineSp)
sp = data[lineSp.xAccessorName]
if(data && lineIdle)
idle = data[lineIdle.xAccessorName]
let spField = <ValueDisplay value={sp}/>
let popContent = linesOther?.map(line =>{
let val = null
if(data)
val = data[line.xAccessorName]
return (
<div key={line.label}>
{line.label}
<ValueDisplay value={val}/>
</div>)
})
if(popContent)
spField = <Popover content={popContent}>
<div style={{display:"flex"}}>
<ControlOutlined className='display_label'/>
{spField}
</div>
</Popover>
else
spField = <div style={{display:"flex"}}>
{spField}
</div>
return(<div>
{spField}
<div style={{display:"flex"}}>
<span className='display_label'>х.х.</span>
<ValueDisplay value={idle}/>
</div>
</div>)
}

View File

@ -0,0 +1,26 @@
import {Display} from './Display'
export const CustomColumn = ({data}) => {
const dataLast = data[data.length -1]
const lines = [
{label:'Рот., об/мин', accessorName:'rotorSpeed'},
{label:'Долото, м', accessorName:'bitDepth'},
{label:'Забой, м', accessorName:'wellDepth'},
{label:'Расход, м³/ч', accessorName:'flow'},
{label:'Расход х.х., м³/ч', accessorName:'flowIdle'},
]
if(dataLast)
lines.forEach(line => line.value = dataLast[line.accessorName]?.toPrecision(4)??'-' )
else
lines.forEach(line => line.value = '-' )
return (<>
{lines.map(line => <Display className='border_small display_flex_container'
kay={line.label}
label={line.label}
value={line.value}
suffix={line.units}/>)}
</>)
}

View File

@ -0,0 +1,23 @@
export const ValueDisplay = ({prefix, value, suffix}) =>{
let val = '---'
if(value)
if(Number.isFinite(+value))
val = (+value).toPrecision(4)??'---'
else
val = value
return(<span className='display_value'>{prefix} {val} {suffix}</span>)
}
export const Display = (props)=>{
const {label} = props
return <div className={props.className}>
<div className='display_label'>{label}</div>
<div style={{display:"flex", flexGrow:1}}>
<ValueDisplay {...props}/>
</div>
</div>
}

View File

@ -0,0 +1,27 @@
const modeNames = {
0: "Ручной",
1: "Бурение в роторе",
2: "Проработка",
3: "Бурение в слайде",
4: "Спуск СПО",
5: "Подъем СПО",
6: "Подъем с проработкой",
10: "БЛОКИРОВКА",
}
export const ModeDisplay = (props)=>{
let value = '---'
if(props.data.length > 0){
let lastFullData = props.data[props.data.length - 1]
let index = lastFullData['mode']
if(index >= 0)
value = modeNames[index] ?? index
}
return(<>
<span className="display_label">Режим:</span>
<span className="display_value">{value}</span>
</>)
}

6
src/components/Well.jsx Normal file
View File

@ -0,0 +1,6 @@
export const Well = (props) => {
return(<div></div>)
}

View File

@ -14,16 +14,17 @@ import 'chartjs-adapter-date-fns';
Chart.register( TimeScale, LinearScale, LineController, LineElement, PointElement, Legend );
const defailtOptions = {
maintainAspectRatio: false,
aspectRatio:0.4,
const defaultOptions = {
//maintainAspectRatio: false,
aspectRatio:0.45,
animation: false,
scales: {
y:{
type: 'time',
reverse:true,
time: {
unit: 'second',
//unit: 'second',
//round:'second',
stepSize: 20,
displayFormats: {
millisecond: 'HH:mm:ss.SSS',
@ -44,7 +45,7 @@ const defailtOptions = {
ticks: {
z: 1,
textStrokeColor : "#ffff",
textStrokeWidth : 1,
textStrokeWidth : 2,
color:"#000",
}
},
@ -84,6 +85,19 @@ export type ChartTimeBaseProps = {
options?: ChartOptions<keyof ChartTypeRegistry>,
}
const timeUnitByInterval = (intervalSec:number):String =>{
if(intervalSec < 24*60*60)
return 'second'
if(intervalSec < 30*24*60*60)
return 'day'
if(intervalSec < 365*24*60*60)
return 'week'
else
return 'year'
}
export const ChartTimeBase: React.FC<ChartTimeBaseProps> = (props) => {
const chartRef = useRef<HTMLCanvasElement>(null)
@ -92,7 +106,7 @@ export const ChartTimeBase: React.FC<ChartTimeBaseProps> = (props) => {
useEffect(()=>{
if(chartRef.current && (!chart)){
let thisOptions = {}
Object.assign(thisOptions, defailtOptions, props.options)
Object.assign(thisOptions, defaultOptions, props.options)
let newChart = new Chart(chartRef.current, {
type: 'line',
@ -104,17 +118,14 @@ export const ChartTimeBase: React.FC<ChartTimeBaseProps> = (props) => {
}
}, [chart, props.options, props.dataParams])
useEffect(()=>{
if(!chart)
return
//mergeDataSets(chart.data, props.dataParams.data)
chart.data = props.dataParams.data
if(props.dataParams.yStart){
let interval = props.dataParams.yInterval ?? 600
let interval = Number(props.dataParams.yInterval ?? 600)
let start = new Date(props.dataParams.yStart)
let end = new Date(props.dataParams.yStart)
end.setSeconds(end.getSeconds() + interval)
@ -123,6 +134,8 @@ export const ChartTimeBase: React.FC<ChartTimeBaseProps> = (props) => {
chart.options.scales.y.max = end.getTime()
chart.options.scales.y.min = start.getTime()
chart.options.scales.y.ticks.display = props.dataParams.displayLabels ?? true
chart.options.scales.y.time.unit = timeUnitByInterval(interval)
chart.options.scales.y.time.stepSize = Math.round(interval/24)
}
}

View File

@ -28,8 +28,9 @@ function GetOrCreateDatasetByLineConfig (data: ChartTimeData, lineConfig: LineC
export type LineConfig = {
type?: string
label: string
xAcessorName: string
yAcessorName: string
units?: string
xAccessorName: string
yAccessorName: string
color?:string
borderColor?: string
backgroundColor?: string
@ -39,14 +40,17 @@ export type LineConfig = {
}
export type ChartTimeProps = {
label?: string,
yDisplay: Boolean,
// linePv?: LineConfig,
// lineSp?: LineConfig,
// lineIdle?: LineConfig,
lines: LineConfig[],
data: any[],
interval: number,
}
export const ChartTime: React.FC<ChartTimeProps> = (props) => {
export const ChartTimeOnline: React.FC<ChartTimeProps> = (props) => {
const [dataParams, setDataParams] = useState<ChartTimeDataParams>({data: {datasets:[]}, yStart: new Date(), })
useEffect(()=>{
@ -56,23 +60,30 @@ export const ChartTime: React.FC<ChartTimeProps> = (props) => {
|| (props.data.length === 0))
return
props.lines.forEach(lineCfg => {
let dataset = GetOrCreateDatasetByLineConfig(dataParams.data, lineCfg)
let points = props.data.map(dataItem => {return{
x: dataItem[lineCfg.xAcessorName],
y: new Date(dataItem[lineCfg.yAcessorName])}
})
dataset.data = [ ...dataset.data, ...points, ].slice(-1024)
});
setDataParams((preDataParams) => {
props.lines.forEach(lineCfg => {
let dataset = GetOrCreateDatasetByLineConfig(preDataParams.data, lineCfg)
let points = props.data.map(dataItem => {return{
x: dataItem[lineCfg.xAccessorName],
y: new Date(dataItem[lineCfg.yAccessorName])}
})
//dataset.data = [ ...dataset.data, ...points,].slice(-1024)
let data = [ ...dataset.data, ...points,]
if(points?.length > 2)
data.sort((a,b) => a.y > b.y ? 1 : -1)
if(data.length > 1024)
data.splice(0, (1024 - data.length))
dataset.data = data;
});
preDataParams.yStart = new Date()
preDataParams.yStart.setSeconds(preDataParams.yStart.getSeconds() - props.interval)
preDataParams.yInterval = props.interval
preDataParams.displayLabels = props.yDisplay
return preDataParams
})
dataParams.yStart = new Date()
dataParams.yStart.setSeconds(dataParams.yStart.getSeconds() - props.interval)
dataParams.yInterval = props.interval
dataParams.displayLabels = props.yDisplay
setDataParams(dataParams)
}, [ props.data, props.lines, props.interval])
}, [ props.data, props.lines, props.interval, props.yDisplay])
return(<ChartTimeBase dataParams = {dataParams} />)
}

View File

@ -4,7 +4,7 @@ import logo from '../images/logo_32.png'
import { useState } from 'react'
import { Switch, Route, Redirect, Link} from "react-router-dom"
import Wells from './Wells'
import Well from './Well'
import TelemetryView from './TelemetryView'
import Files from './Files'
const { Header, Content, Sider } = Layout
@ -43,7 +43,19 @@ export default function Main(){
<Link to="/well">Мониторинг</Link>
</Menu.Item>
<Menu.Item key="2" icon= {<FolderOutlined />}>
<Link to="/file">файлы</Link>
<Link to="/file">Архив</Link>
</Menu.Item>
<Menu.Item key="3" icon= {<FolderOutlined />}>
<Link to="/file">Сообщения</Link>
</Menu.Item>
<Menu.Item key="4" icon= {<FolderOutlined />}>
<Link to="/file">Рапорт</Link>
</Menu.Item>
<Menu.Item key="5" icon= {<FolderOutlined />}>
<Link to="/file">Анализ</Link>
</Menu.Item>
<Menu.Item key="6" icon= {<FolderOutlined />}>
<Link to="/file">Файлы</Link>
</Menu.Item>
</Menu>
</Sider>
@ -56,7 +68,7 @@ export default function Main(){
<Files />
</Route>
<Route path="/well/:id">
<Well/>
<TelemetryView/>
</Route>
<Route path="/well">
<Wells />

158
src/pages/TelemetryView.jsx Normal file
View File

@ -0,0 +1,158 @@
import { useState, useEffect} from 'react';
import { Row, Col, Select, } from 'antd'
import { ChartTimeOnline} from '../components/charts/ChartTimeOnline'
import { ChartTimeOnlineFooter} from '../components/ChartTimeOnlineFooter'
import { CustomColumn} from '../components/CustomColumn'
import { ModeDisplay } from '../components/ModeDisplay';
import { Display } from '../components/Display';
import { useParams} from 'react-router-dom'
import { Subscribe} from '../services/signalr'
import { DataService} from '../services/api'
const {Option} = Select
const dash = [7,3]
const blockHeightGroup = {
label:"Высота блока",
yDisplay: false,
linePv: { label:"blockHeight", units:'м', xAccessorName : "blockHeight", yAccessorName : "date", color:'#333' },
}
const blockSpeedGroup = {
label:"Скорость блока",
yDisplay: false,
linePv: { label:"blockSpeed", units:'м/ч', xAccessorName : "blockSpeed", yAccessorName : "date", color:'#0a0' },
lineSp: { label:"blockSpeedSp", units:'м/ч', xAccessorName : "blockSpeedSp", yAccessorName : "date", color:'#0a0' },
}
const pressureGroup = {
label:"Давтение",
yDisplay: false,
linePv: { label:"pressure", units:'атм', xAccessorName : "pressure", yAccessorName : "date", color:'#c00' },
lineSp: { label:"pressureSp", units:'атм', xAccessorName : "pressureSp", yAccessorName : "date", color:'#c00' },
lineIdle: { label:"pressureIdle", units:'атм', xAccessorName : "pressureIdle", yAccessorName : "date", color:'#c00' },
linesOther: [
{ label:"мекс. перепад", units:'атм', xAccessorName : "pressureDeltaLimitMax", yAccessorName : "date", color:'#c00' },
],
}
const axialLoadGroup = {
label:"Осевая нагрузка",
yDisplay: false,
linePv: { label:"axialLoad", units:'т', xAccessorName : "axialLoad", yAccessorName : "date", color:'#00a' },
lineSp: { label:"axialLoadSp", units:'т', xAccessorName : "axialLoadSp", yAccessorName : "date", color:'#00a', dash },
linesOther: [
{ label:"axialLoadLimitMax", units:'т', xAccessorName : "axialLoadLimitMax", yAccessorName : "date", color:'#00a' },
],
}
const hookWeightGroup = {
label:"Ввес на крюке",
yDisplay: false,
linePv: { label:"hookWeight", units:'т', xAccessorName : "hookWeight", yAccessorName : "date", color:'#0aa' },
lineIdle: { label:"hookWeightIdle", units:'т', xAccessorName : "hookWeightIdle", yAccessorName : "date", color:'#0aa', dash },
linesOther: [
{ label:"hookWeightLimitMin", units:'т', xAccessorName : "hookWeightLimitMin", yAccessorName : "date", color:'#0aa' },
{ label:"hookWeightLimitMax", units:'т', xAccessorName : "hookWeightLimitMax", yAccessorName : "date", color:'#0aa' },
],
}
const rotorTorqueGroup = {
label:"Момент на роторе",
yDisplay: false,
linePv: { label:"rotorTorque", units:'кН·м', xAccessorName : "rotorTorque", yAccessorName : "date", color:'#a0a' },
lineSp: { label:"rotorTorqueSp", units:'кН·м', xAccessorName : "rotorTorqueSp", yAccessorName : "date", color:'#a0a' },
lineIdle: { label:"rotorTorqueIdle", units:'кН·м', xAccessorName : "rotorTorqueIdle", yAccessorName : "date", color:'#a0a' },
linesOther: [
{ label:"rotorTorqueLimitMax", units:'кН·м', xAccessorName : "rotorTorqueLimitMax", yAccessorName : "date", color:'#a0a' },
],
}
const paramsGroups = [blockHeightGroup, blockSpeedGroup, pressureGroup, axialLoadGroup, hookWeightGroup, rotorTorqueGroup]
export const Column = ({lineGroup, data, interval})=>{
let lines = [lineGroup.linePv]
if(lineGroup.lineSp)
lines.push(lineGroup.lineSp)
let dataLast = null
let pv = null
if(data?.length > 0 ){
dataLast = data[data.length - 1];
if(lineGroup.linePv)
pv = dataLast[lineGroup.linePv?.xAccessorName]
}
return(
<>
<Display
label={lineGroup.label}
value={pv}
suffix={lineGroup.linePv?.units}/>
<ChartTimeOnline
data={data}
yDisplay={lineGroup.yDisplay}
lines={lines}
interval={interval}/>
<ChartTimeOnlineFooter
data={dataLast}
{...lineGroup} />
</>)
}
export default function TelemetryView(props){
let { id } = useParams();
const [saubData, setSaubData] = useState([])
const [chartInterval, setchartInterval] = useState(600)
const handleReceiveDataSaub = (data)=>{
if(data){
setSaubData(data)
}
}
useEffect( ()=> {
DataService.get(id)
.then(handleReceiveDataSaub)
.catch(error=>console.error(error))
return Subscribe('ReceiveDataSaub', `well_${id}`, handleReceiveDataSaub)
}, [id]);
const colSpan = 24/(paramsGroups.length )
return(<div>
{/* <div>Well id: {id}; points count: {saubData.length}</div> */}
<Row style={{marginBottom:'1rem'}}>
<Col>
<ModeDisplay data={saubData}/>
</Col>
<Col>
Интервал:&nbsp;
<Select defaultValue="600" onChange={setchartInterval}>
<Option value='600'>10 минут</Option>
<Option value='1800'>30 минут</Option>
<Option value='3600'>1 час</Option>
<Option value='21600'>6 час</Option>
<Option value='86400'>1 день</Option>
</Select>
</Col>
</Row>
<Row>
<Col span={2} >
<CustomColumn data={saubData} />
</Col>
<Col span={24 - 2}>
<Row>
{paramsGroups.map(group=>
<Col span={colSpan} className='border_small' key={group.label}>
<Column data={saubData} lineGroup={group} interval={chartInterval}/>
</Col>)}
</Row>
</Col>
</Row>
</div>)
}

View File

@ -1,86 +0,0 @@
import React, { useState, useEffect,} from 'react';
import { Row, Col} from 'antd'
import {ChartTime} from '../components/charts/ChartTime'
import {useParams} from 'react-router-dom'
import {Subscribe} from '../services/signalr'
import {DataService} from '../services/api'
const dash = [7,3]
const lineParameters = [
{
yDisplay: true,
lines:[
{ label:"blockHeight", xAcessorName : "blockHeight", yAcessorName : "date", color:'#aaf' },
],
},
{
yDisplay: false,
lines:[
{ label:"blockSpeed", xAcessorName : "blockSpeed", yAcessorName : "date", color:'#0a0' },
{ label:"blockSpeedSp", xAcessorName : "blockSpeedSp", yAcessorName : "date", color:'#0a0', dash },
],
},
{
yDisplay: false,
lines:[
{ label:"pressure", xAcessorName : "pressure", yAcessorName : "date", color:'#a00' },
{ label:"pressureSp", xAcessorName : "pressureSp", yAcessorName : "date", color:'#a00', dash },
],
},
{
yDisplay: false,
lines:[
{ label:"axialLoad", xAcessorName : "axialLoad", yAcessorName : "date", color:'#00a' },
{ label:"axialLoadSp", xAcessorName : "axialLoadSp", yAcessorName : "date", color:'#00a', dash },
],
},
{
yDisplay: false,
lines:[
{ label:"вес", xAcessorName : "hookWeight", yAcessorName : "date", color:'#0aa' },
{ label:"вес макс", xAcessorName : "hookWeightLimitMax", yAcessorName : "date", color:'#0aa', dash },
],
},
{
yDisplay: false,
lines:[
{ label:"rotorTorque", xAcessorName : "rotorTorque", yAcessorName : "date", color:'#a0a' },
{ label:"rotorTorqueSp", xAcessorName : "rotorTorqueSp", yAcessorName : "date", color:'#a0a', dash },
],
}
]
const interval = 600
export default function Well(props){
let { id } = useParams();
const [saubData, setSaubData] = useState([])
const handleReceiveDataSaub = (data)=>{
if(data)
setSaubData(data)
}
useEffect( ()=> {
DataService.get(id)
.then(handleReceiveDataSaub)
.catch(error=>console.error(error))
return Subscribe('ReceiveDataSaub', `well_${id}`, handleReceiveDataSaub)
},
[id]);
let colSpan = 24/lineParameters.length
let cols = lineParameters.map(line =>
<Col span={colSpan}>
<ChartTime data={saubData} {...line} interval={interval}/>
</Col>)
return(<div>
<div>Well id: {id}; points count: {saubData.length}</div>
<Row height='100%'>{cols}</Row>
</div>)
}

View File

@ -58,19 +58,44 @@
min-height: 280;
}
#components-layout-demo-top-side-2 .logo {
float: left;
width: 120px;
height: 31px;
margin: 16px 24px 16px 0;
background: rgba(255, 255, 255, 0.3);
}
.ant-row-rtl #components-layout-demo-top-side-2 .logo {
float: right;
margin: 16px 0 16px 24px;
}
.site-layout-background {
background: #fff;
}
.border_small{
border: 1px solid rgba(0, 0, 0, 0.05);
}
.display_flex_container{
display: flex;
flex-wrap: wrap;
flex: auto;
}
.display_label{
font-size: 16px;
color: rgb(70, 70, 70);
text-align: left;
justify-content: baseline;
margin: 1px 1rem 1px 1rem;
flex: auto;
}
.display_value{
font-size: 18px;
font-weight: bold;
color: rgb(50, 50, 50);
text-align: right;
justify-content: flex-end;
align-items:baseline;
margin: 1px 1rem 1px 1rem;
flex: auto;
}
.display_small_value{
color: rgb(50, 50, 50);
text-align: right;
justify-content: baseline;
margin: 1px 1rem 1px 1rem;
flex: auto;
}