Обработка данных графика перенесена в Column.jsx, зоны закончены

This commit is contained in:
goodmice 2021-10-27 11:05:50 +05:00
parent 35136f6269
commit 1ee38ca733
5 changed files with 230 additions and 211 deletions

View File

@ -1,4 +1,4 @@
import { useEffect, useRef, useState} from 'react';
import { useEffect, useRef, useState} from 'react'
import {
Chart,
TimeScale,
@ -10,11 +10,20 @@ import {
ChartData,
ChartTypeRegistry,
ChartOptions} from 'chart.js'
import 'chartjs-adapter-moment';
import ChartDataLabels from 'chartjs-plugin-datalabels';
import zoomPlugin from 'chartjs-plugin-zoom';
import 'chartjs-adapter-moment'
import ChartDataLabels from 'chartjs-plugin-datalabels'
import zoomPlugin from 'chartjs-plugin-zoom'
Chart.register( TimeScale, LinearScale, LineController, LineElement, PointElement, Legend, ChartDataLabels, zoomPlugin );
Chart.register(
TimeScale,
LinearScale,
LineController,
LineElement,
PointElement,
Legend,
ChartDataLabels,
zoomPlugin
)
const defaultOptions = {
responsive: true,
@ -23,9 +32,7 @@ const defaultOptions = {
tooltips: {
enabled: true,
callbacks: {
label(tooltipItem:any) {
return tooltipItem.yLabel;
}
label: (tooltipItem:any) => tooltipItem.yLabel
}
},
events: ['mousemove', 'mouseout', 'click', 'touchstart', 'touchmove'],
@ -53,9 +60,9 @@ const defaultOptions = {
ticks: {
z: 1,
display : false,
textStrokeColor : "#ffff",
textStrokeColor : '#ffff',
textStrokeWidth : 2,
color:"#000",
color:'#000',
}
},
@ -118,7 +125,7 @@ export type TimeParams = {
const linesPerInterval = 32
export const timeUnitByInterval = (intervalSec:number):String => {
export const timeUnitByInterval = (intervalSec: number): String => {
if(intervalSec <= 60)
return 'millisecond'
@ -146,42 +153,42 @@ export const timeUnitByInterval = (intervalSec:number):String => {
return 'year'
}
export const timeParamsByInterval = (intervalSec:number) :TimeParams => {
export const timeParamsByInterval = (intervalSec: number): TimeParams => {
let stepSize = intervalSec
let unit = timeUnitByInterval(intervalSec)
switch(unit){
case "millisecond":
case 'millisecond':
stepSize *= 1000
break;
case "second":
case 'second':
//stepSize *= 1
break;
case "minute":
case 'minute':
stepSize /= 60
break;
case "hour":
case 'hour':
stepSize /= 60*60
break;
case "day":
case 'day':
stepSize /= 24*60*60
break;
case "week":
case 'week':
stepSize /= 7*24*60*60
break;
case "month":
case 'month':
stepSize /= 30*24*60*60
break;
case "quarter":
case 'quarter':
stepSize /= 91*24*60*60
break;
case "year":
case 'year':
stepSize /= 365.25*24*60*60
break;
}
stepSize = Math.round(stepSize/linesPerInterval)
stepSize = stepSize > 0 ? stepSize : 1;
stepSize = stepSize > 0 ? stepSize : 1
return {unit, stepSize}
}
@ -204,20 +211,17 @@ export const ChartTimeBase: React.FC<ChartTimeBaseProps> = ({options, dataParams
return () => chart?.destroy()
}
}, [chart, options, dataParams])
useEffect(()=>{
if(!chart)
return
if(!chart) return
chart.data = dataParams.data
chart.options.aspectRatio = options?.aspectRatio
if(dataParams.yStart){
let interval = Number(dataParams.yInterval ?? 600)
let start = new Date(dataParams.yStart)
let end = new Date(dataParams.yStart)
const interval = Number(dataParams.yInterval ?? 600)
const start = new Date(dataParams.yStart)
const end = new Date(dataParams.yStart)
end.setSeconds(end.getSeconds() + interval)
let {unit, stepSize} = timeParamsByInterval(interval)
const { unit, stepSize } = timeParamsByInterval(interval)
if(chart.options.scales?.y){
chart.options.scales.y.max = end.getTime()

View File

@ -1,26 +1,24 @@
import { useParams } from "react-router-dom";
import { useState, useEffect } from "react";
import ClusterWells from "./ClusterWells";
import LoaderPortal from "../../components/LoaderPortal";
import { invokeWebApiWrapperAsync } from "../../components/factory";
import { WellOperationStatService } from "../../services/api";
import { useParams } from 'react-router-dom'
import { useState, useEffect } from 'react'
import ClusterWells from './ClusterWells'
import LoaderPortal from '../../components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '../../components/factory'
import { WellOperationStatService } from '../../services/api'
export default function Cluster() {
let { idClaster } = useParams();
const [data, setData] = useState([]);
const [showLoader, setShowLoader] = useState(false);
export const Cluster = () => {
const { idClaster } = useParams()
const [data, setData] = useState([])
const [showLoader, setShowLoader] = useState(false)
useEffect(() => invokeWebApiWrapperAsync(
async () => {
const clusterData = await WellOperationStatService.getStatCluster(idClaster);
setData(clusterData?.statsWells ?? []);
const clusterData = await WellOperationStatService.getStatCluster(idClaster)
setData(clusterData?.statsWells ?? [])
},
setShowLoader,
`Не удалось загрузить данные по кусту "${idClaster}"`
), [idClaster]);
useEffect(() => console.log(data), [data])
), [idClaster])
return (
<LoaderPortal show={showLoader}>
@ -28,3 +26,5 @@ export default function Cluster() {
</LoaderPortal>
)
}
export default Cluster

View File

@ -1,121 +0,0 @@
import { useEffect, useState} from 'react';
import {ChartTimeBase, ChartTimeData, ChartTimeDataParams} from '../../components/charts/ChartTimeBase'
const GetRandomColor = () => "#" + Math.floor(Math.random()*16777215).toString(16)
function GetOrCreateDatasetByLineConfig (data: ChartTimeData, lineConfig: LineConfig) {
let dataset = data?.datasets.find(d=>d.label === lineConfig.label)
if(!dataset)
{
let color = lineConfig.borderColor
?? lineConfig.backgroundColor
?? lineConfig.color
?? GetRandomColor()
dataset = {
label:lineConfig.label,
data:[],
backgroundColor: lineConfig.backgroundColor ?? color,
borderColor: lineConfig.borderColor ?? color,
borderWidth: lineConfig.borderWidth ?? 1,
borderDash: lineConfig.dash ?? [],
showLine: lineConfig.showLine,
}
data.datasets.push(dataset);
}
return dataset
}
export type LineConfig = {
type?: string
label: string
units?: string
xAccessorName: string
yAccessorName: string
color?:string
borderColor?: string
backgroundColor?: string
borderWidth?: number
dash?:number[]
labels?: any[]
showLine: boolean
xConstValue?: number|null
}
export type ChartTimeProps = {
label?: string,
yDisplay: Boolean,
// linePv?: LineConfig,
// lineSp?: LineConfig,
// lineIdle?: LineConfig,
lines: LineConfig[],
data: any[],
interval: number,
}
export const ChartTimeOnline: React.FC<ChartTimeProps> = (props) => {
const [dataParams, setDataParams] = useState<ChartTimeDataParams>({data: {datasets:[]}, yStart: new Date(), })
useEffect(()=>{
if( (!props?.lines)
|| (!props?.data)
|| (props.lines.length === 0)
|| (props.data.length === 0))
return
setDataParams((preDataParams) => {
props.lines.forEach(lineCfg => {
let dataset = GetOrCreateDatasetByLineConfig(preDataParams.data, lineCfg)
let points = props.data.map(dataItem => {return{
x: lineCfg.xConstValue ?? dataItem[lineCfg.xAccessorName],
label: 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
})
}, [ props.data, props.lines, props.interval, props.yDisplay])
const chartPluginsOptions = {
plugins:{
legend:{
display: false,
},
datalabels: {
backgroundColor: 'transparent',
borderRadius: 4,
color: '#000B',
display: function(context:any) {
return context.dataset.label === 'wellDepth'
? 'auto'
: false
},
formatter: function(value: any, context: any) {
return `${value.y.toLocaleTimeString()} ${value.label.toPrecision(4)}`
},
padding: 6,
align: 'left',
anchor: 'center',
clip: true
},
tooltip: {
enable: true
}
}
}
return(<ChartTimeBase dataParams = {dataParams} options = { chartPluginsOptions } />)
}

View File

@ -1,37 +1,129 @@
import {Grid, GridItem} from '../../components/Grid';
import {ChartTimeOnline} from './ChartTimeOnline';
import {ChartTimeOnlineFooter} from './ChartTimeOnlineFooter';
import { useState, useEffect } from 'react'
import { Grid, GridItem } from '../../components/Grid'
import { ChartTimeBase } from '../../components/charts/ChartTimeBase'
import { ChartTimeOnlineFooter } from './ChartTimeOnlineFooter'
const stroke = (sz='1px', c='white') => ({ textShadow: `-${sz} -${sz} 0 ${c}, ${sz} -${sz} 0 ${c}, -${sz} ${sz} 0 ${c}, ${sz} ${sz} 0 ${c}` })
const stroke = (sz = '1px', c = 'white') => ({ textShadow: `-${sz} -${sz} 0 ${c}, ${sz} -${sz} 0 ${c}, -${sz} ${sz} 0 ${c}, ${sz} ${sz} 0 ${c}` })
export const Column = ({lineGroup, data, interval, showBorder, style, headerHeight}) => {
let dataLast = data?.length > 0 ? data[data.length - 1] : null
let pv = lineGroup.filter(line => line.showGraph).map(line => ({
const chartPluginsOptions = {
plugins: {
datalabels: {
backgroundColor: 'transparent',
borderRadius: 4,
color: '#000B',
display: context => (context.dataset.label === 'wellDepth') && 'auto',
formatter: value => `${value.y.toLocaleTimeString()} ${value.label.toPrecision(4)}`,
padding: 6,
align: 'left',
anchor: 'center',
clip: true
},
legend:{ display: false },
tooltip: { enable: true }
}
}
const GetLimitShape = (flowChartData, points, accessor) => {
const min = [], max = []
for (let point of points) {
const program = flowChartData.find(v => v.depthStart < point.depth && point.depth < v.depthEnd)
if (!program) continue
min.push({ x: program[`${accessor}Min`], y: new Date(point.y), label: point.label })
max.push({ x: program[`${accessor}Max`], y: new Date(point.y), label: point.label })
}
return min.concat(max.reverse()) ?? []
}
const GetRandomColor = () => '#' + Math.floor(Math.random()*16777215).toString(16)
const GetOrCreateDatasetByLineConfig = (data, lineConfig) => {
let dataset = data?.datasets.find(d => d.label === lineConfig.label)
if(!dataset) {
let color = lineConfig.borderColor
?? lineConfig.backgroundColor
?? lineConfig.color
?? GetRandomColor()
dataset = {
label: lineConfig.label,
data: [],
backgroundColor: lineConfig.backgroundColor ?? color,
borderColor: lineConfig.borderColor ?? color,
borderWidth: lineConfig.borderWidth ?? 1,
borderDash: lineConfig.dash ?? [],
showLine: lineConfig.showLine ?? !lineConfig.isShape,
fill: lineConfig.fill ?? (lineConfig.isShape ? 'shape' : 'none')
}
data.datasets.push(dataset);
}
return dataset
}
export const Column = ({ lineGroup, data, flowChartData, interval, showBorder, style, headerHeight, yDisplay }) => {
const [dataParams, setDataParams] = useState({data: {datasets:[]}, yStart: new Date(), })
let dataLast = data?.[data.length - 1]
let pv = lineGroup.filter(line => line.showLabels).map(line => ({
color: line.color,
label: line.label,
unit: line.units,
value: dataLast?.[line.xAccessorName]
}))
useEffect(()=>{
if((lineGroup.length === 0) || (data.length === 0)) return
setDataParams((preDataParams) => {
lineGroup.forEach(lineCfg => {
if (lineCfg.isShape) return
const dataset = GetOrCreateDatasetByLineConfig(preDataParams.data, lineCfg)
const points = data.map(dataItem => ({
x: lineCfg.xConstValue ?? dataItem[lineCfg.xAccessorName],
label: dataItem[lineCfg.xAccessorName],
y: new Date(dataItem[lineCfg.yAccessorName]),
depth: dataItem.wellDepth
})).filter(point => (point.x ?? null) !== null && (point.y ?? null) !== null)
const lineData = [ ...dataset.data, ...points,]
if(points?.length > 2)
lineData.sort((a,b) => a.y > b.y ? 1 : -1)
if(lineData.length > 1024)
lineData.splice(0, (1024 - lineData.length))
dataset.data = lineData
//Area
lineGroup.filter(cfg => cfg.isShape && cfg.xAccessorName === lineCfg.xAccessorName).forEach(areaCfg => {
const dataset = GetOrCreateDatasetByLineConfig(preDataParams.data, areaCfg)
dataset.data = GetLimitShape(flowChartData, lineData, areaCfg.xAccessorName)
})
})
preDataParams.yStart = new Date()
preDataParams.yStart.setSeconds(preDataParams.yStart.getSeconds() - interval)
preDataParams.yInterval = interval
preDataParams.displayLabels = yDisplay ?? false
return {...preDataParams}
})
}, [data, lineGroup, interval, yDisplay, flowChartData])
return (
<div style={style}>
<Grid style={{height: headerHeight, boxShadow: showBorder ? 'inset 0px 0px 0px 3px black' : ''}}>
<Grid style={{ height: headerHeight, boxShadow: showBorder ? 'inset 0px 0px 0px 3px black' : '' }}>
{pv?.map((v, idx) => (
<GridItem className={'display_chart_label'} key={idx} row={idx} col={1} style={{color: v.color}}>{v.label}</GridItem>
<GridItem className={'display_chart_label'} key={idx} row={idx} col={1} style={{ color: v.color }}>{v.label}</GridItem>
))}
</Grid>
<div style={{position: 'relative'}}>
<div style={{ position: 'relative' }}>
<Grid className={'display_chart_values'}>
{pv?.map((v, idx) => (
<GridItem key={idx} col={1} row={idx} style={{...stroke(), color: v.color, padding: '0 4px'}}>{v.value?.toFixed(2) ?? '--'} {v.unit}</GridItem>
<GridItem key={idx} col={1} row={idx} style={{ ...stroke(), color: v.color, padding: '0 4px' }}>{v.value?.toFixed(2) ?? '--'} {v.unit}</GridItem>
))}
</Grid>
<ChartTimeOnline
data={data}
yDisplay={false}
lines={lineGroup}
interval={interval}
/>
<ChartTimeBase dataParams = {dataParams} options = { chartPluginsOptions } />
</div>
<ChartTimeOnlineFooter data={dataLast} lineGroup={lineGroup} />
</div>

View File

@ -10,7 +10,12 @@ import { UserOfWell } from './UserOfWells'
import LoaderPortal from '../../components/LoaderPortal'
import { Grid, GridItem, Flex } from '../../components/Grid'
import { Subscribe } from '../../services/signalr'
import { TelemetryDataSaubService, TelemetryDataSpinService, WellService } from '../../services/api'
import {
DrillFlowChartService,
TelemetryDataSaubService,
TelemetryDataSpinService,
WellService
} from '../../services/api'
import { invokeWebApiWrapperAsync } from '../../components/factory'
import MomentStabPicEnabled from '../../images/DempherOn.png'
@ -31,7 +36,7 @@ const blockHeightGroup = [
xAccessorName: 'blockPosition',
yAccessorName: 'date',
color: '#333',
showGraph: true
showLabels: true
}, {
label: 'wellDepth',
units: 'м',
@ -47,7 +52,15 @@ const blockHeightGroup = [
xAccessorName: 'flow',
yAccessorName: 'date',
color: '#077',
showGraph: true
showLabels: true,
showLine: true
}, {
label: 'flowLimits',
units: 'л/с',
xAccessorName: 'flow',
yAccessorName: 'date',
color: 'rgba(0,119,119,.1)',
isShape: true
}
]
@ -58,7 +71,7 @@ const blockSpeedGroup = [
xAccessorName: 'blockSpeed',
yAccessorName: 'date',
color: '#0a0',
showGraph: true
showLabels: true,
}, {
label: 'blockSpeedSp',
units: 'м/ч',
@ -77,7 +90,7 @@ const pressureGroup = [
xAccessorName: 'pressure',
yAccessorName: 'date',
color: '#c00',
showGraph: true
showLabels: true
}, {
label: 'pressureSp',
units: 'атм',
@ -102,6 +115,13 @@ const pressureGroup = [
color: '#c00',
footer: true,
dash
}, {
label: 'pressureLimits',
units: 'атм',
xAccessorName: 'pressure',
yAccessorName: 'date',
color: 'rgba(204,0,0,.1)',
isShape: true
}
]
@ -112,7 +132,7 @@ const axialLoadGroup = [
xAccessorName: 'axialLoad',
yAccessorName: 'date',
color: '#00a',
showGraph: true
showLabels: true
}, {
label: 'axialLoadSp',
units: 'т',
@ -129,7 +149,14 @@ const axialLoadGroup = [
color: '#00a',
footer: true,
dash
},
}, {
label: 'axialLoadLimits',
units: 'т',
xAccessorName: 'axialLoad',
yAccessorName: 'date',
color: 'rgba(0,0,170,.1)',
isShape: true
}
]
const hookWeightGroup = [
@ -139,7 +166,7 @@ const hookWeightGroup = [
xAccessorName: 'hookWeight',
yAccessorName: 'date',
color: '#0aa',
showGraph: true
showLabels: true
}, {
label: 'hookWeightIdle',
units: 'т',
@ -164,14 +191,20 @@ const hookWeightGroup = [
color: '#0aa',
footer: true,
dash
},
{
}, {
label: 'Обороты ротора',
units: 'об/мин',
xAccessorName: 'rotorSpeed',
yAccessorName: 'date',
color: '#aa0',
showGraph: true
showLabels: true
}, {
label: 'rotorSpeedLimits',
units: 'об/мин',
xAccessorName: 'rotorSpeed',
yAccessorName: 'date',
color: 'rgba(170,170,0,.1)',
isShape: true
}
]
@ -182,7 +215,7 @@ const rotorTorqueGroup = [
xAccessorName: 'rotorTorque',
yAccessorName: 'date',
color: '#a0a',
showGraph: true
showLabels: true
}, {
label: 'План. Момент на роторе',
units: 'кН·м',
@ -207,7 +240,14 @@ const rotorTorqueGroup = [
color: '#a0a',
footer: true,
dash
},
}, {
label: 'rotorTorqueLimits',
units: 'кН·м',
xAccessorName: 'rotorTorque',
yAccessorName: 'date',
color: 'rgba(170,0,170,.1)',
isShape: true
}
]
const paramsGroups = [blockHeightGroup, blockSpeedGroup, pressureGroup, axialLoadGroup, hookWeightGroup, rotorTorqueGroup]
@ -259,8 +299,9 @@ export default function TelemetryView({ idWell }) {
const [dataSaub, setDataSaub] = useState([])
const [dataSpin, setDataSpin] = useState([])
const [chartInterval, setChartInterval] = useState(defaultChartInterval)
const [wellData, setWellData] = useState({idState: 0})
const [wellData, setWellData] = useState({ idState: 0 })
const [showLoader, setShowLoader] = useState(false)
const [flowChartData, setFlowChartData] = useState([])
const options = timePeriodCollection.map((line) => <Option key={line.value}>{line.label}</Option>)
@ -284,18 +325,20 @@ export default function TelemetryView({ idWell }) {
}
useEffect(() => {
const unsubscribeSaub = Subscribe('hubs/telemetry', 'ReceiveDataSaub', `well_${idWell}`, handleDataSaub)
const unsubscribeSpin = Subscribe('hubs/telemetry', 'ReceiveDataSpin', `well_${idWell}`, handleDataSpin)
invokeWebApiWrapperAsync(
async () => {
const flowChart = await DrillFlowChartService.get(idWell)
const dataSaub = await TelemetryDataSaubService.getData(idWell, null, chartInterval)
const dataSpin = await TelemetryDataSpinService.getData(idWell, null, chartInterval)
setFlowChartData(flowChart ?? [])
handleDataSaub(dataSaub)
handleDataSpin(dataSpin)
},
setShowLoader,
null,
`Не удалось получить данные по скважине "${idWell}"`,
)
const unsubscribeSaub = Subscribe('hubs/telemetry', 'ReceiveDataSaub', `well_${idWell}`, handleDataSaub)
const unsubscribeSpin = Subscribe('hubs/telemetry', 'ReceiveDataSpin', `well_${idWell}`, handleDataSpin)
return () => {
unsubscribeSaub()
unsubscribeSpin()
@ -312,15 +355,15 @@ export default function TelemetryView({ idWell }) {
), [idWell])
const onStatusChanged = (value) => {
invokeWebApiWrapperAsync(
async () => {
const well = {...wellData, idState: value}
await WellService.updateWell(idWell, well)
setWellData(well)
},
setShowLoader,
`Не удалось задать состояние скважины "${idWell}"`
)
invokeWebApiWrapperAsync(
async () => {
const well = { ...wellData, idState: value }
await WellService.updateWell(idWell, well)
setWellData(well)
},
setShowLoader,
`Не удалось задать состояние скважины "${idWell}"`
)
}
return (
@ -335,9 +378,9 @@ export default function TelemetryView({ idWell }) {
{options}
</Select>
</div>
<div style={{ marginLeft: '1rem'}}>
<div style={{ marginLeft: '1rem' }}>
Статус:&nbsp;
<Select value={wellData.idState} onChange={onStatusChanged}>
<Select value={wellData.idState ?? 0} onChange={onStatusChanged}>
<Option value={0} disabled>Неизвестно</Option>
<Option value={1}>В работе</Option>
<Option value={2}>Завершено</Option>
@ -358,6 +401,7 @@ export default function TelemetryView({ idWell }) {
<Column
style={{ width: '13vw' }}
data={dataSaub}
flowChartData={flowChartData}
lineGroup={group}
interval={chartInterval}
headerHeight={'60px'}