diff --git a/src/components/charts/ChartTimeBase.tsx b/src/components/charts/ChartTimeBase.tsx index eae15c3..b3a8621 100644 --- a/src/components/charts/ChartTimeBase.tsx +++ b/src/components/charts/ChartTimeBase.tsx @@ -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 = ({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() diff --git a/src/pages/Cluster/index.jsx b/src/pages/Cluster/index.jsx index 1a2e0bb..ea2a8a6 100644 --- a/src/pages/Cluster/index.jsx +++ b/src/pages/Cluster/index.jsx @@ -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 ( @@ -28,3 +26,5 @@ export default function Cluster() { ) } + +export default Cluster diff --git a/src/pages/TelemetryView/ChartTimeOnline.tsx b/src/pages/TelemetryView/ChartTimeOnline.tsx deleted file mode 100644 index 7aa1d35..0000000 --- a/src/pages/TelemetryView/ChartTimeOnline.tsx +++ /dev/null @@ -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 = (props) => { - const [dataParams, setDataParams] = useState({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() -} - diff --git a/src/pages/TelemetryView/Column.jsx b/src/pages/TelemetryView/Column.jsx index 8b2ccf1..4c9d790 100644 --- a/src/pages/TelemetryView/Column.jsx +++ b/src/pages/TelemetryView/Column.jsx @@ -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 (
- + {pv?.map((v, idx) => ( - {v.label} + {v.label} ))} -
+
{pv?.map((v, idx) => ( - {v.value?.toFixed(2) ?? '--'} {v.unit} + {v.value?.toFixed(2) ?? '--'} {v.unit} ))} - +
diff --git a/src/pages/TelemetryView/index.jsx b/src/pages/TelemetryView/index.jsx index 58f697d..f9048f8 100644 --- a/src/pages/TelemetryView/index.jsx +++ b/src/pages/TelemetryView/index.jsx @@ -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) => ) @@ -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}
-
+
Статус:  - @@ -358,6 +401,7 @@ export default function TelemetryView({ idWell }) {