From 5131259b3f80c9300de3383224b524bb07296fda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A4=D1=80=D0=BE=D0=BB=D0=BE=D0=B2?= Date: Fri, 28 May 2021 12:04:50 +0500 Subject: [PATCH] Add archive page --- src/components/ArchiveColumn.jsx | 123 +++++++++ src/components/WellTreeSelector.jsx | 3 + src/components/charts/ChartTimeArchive.jsx | 73 ++++++ src/components/charts/ChartTimeArchive.tsx | 82 ------ src/components/charts/ChartTimeBase.tsx | 38 +-- src/pages/Archive.jsx | 289 +++++++++------------ src/services/UidGenerator.ts | 13 + 7 files changed, 352 insertions(+), 269 deletions(-) create mode 100644 src/components/ArchiveColumn.jsx create mode 100644 src/components/charts/ChartTimeArchive.jsx delete mode 100644 src/components/charts/ChartTimeArchive.tsx create mode 100644 src/services/UidGenerator.ts diff --git a/src/components/ArchiveColumn.jsx b/src/components/ArchiveColumn.jsx new file mode 100644 index 0000000..d2f9b1a --- /dev/null +++ b/src/components/ArchiveColumn.jsx @@ -0,0 +1,123 @@ +import { useState, useEffect } from 'react'; +import { Button, Select, Tag, Popover, Row, Tooltip } from 'antd'; +import { ChartTimeArchive } from './charts/ChartTimeArchive'; +//import { SlidersOutlined } from '@ant-design/icons'; + +const { Option } = Select; + +const linesCollection = [ + { label: "Глубина забоя", xAccessorName: "wellDepth", color: '#f00' }, + { label: "Положение инструмента", xAccessorName: "bitDepth", color: '#ff0' }, + { label: "Высота талевого блока", xAccessorName: "blockPosition", color: '#f0f' }, + { label: "Талевый блок. Мин положение", xAccessorName: "blockPositionMin", color: '#0ff' }, + { label: "Талевый блок. Макс положение", xAccessorName: "blockPositionMax", color: '#0f0' }, + { label: "Скорость талевого блока", xAccessorName: "blockSpeed", color: '#00f' }, + + { label: "Скорости талевого блока. Задание", xAccessorName: "blockSpeedSp", color: '#c00' }, + { label: "Талевый блок. Задание скорости для роторного бурения", xAccessorName: "blockSpeedSpRotor", color: '#cc0' }, + { label: "Талевый блок. Задание скорости для режима слайда", xAccessorName: "blockSpeedSpSlide", color: '#c0c' }, + { label: "Талевый блок. Задание скорости для проработки", xAccessorName: "blockSpeedSpDevelop", color: '#0cc' }, + { label: "Давление", xAccessorName: "pressure", color: '#0c0' }, + { label: "Давление. Холостой ход", xAccessorName: "pressureIdle", color: '#00c' }, + + { label: "Давление. Задание", xAccessorName: "pressureSp", color: '#900' }, + { label: "Давление. Задание для роторного бурения", xAccessorName: "pressureSpRotor", color: '#990' }, + { label: "Давление. Задание для режима слайда", xAccessorName: "pressureSpSlide", color: '#909' }, + { label: "Давление. Задание для проработки", xAccessorName: "pressureSpDevelop", color: '#099' }, + { label: "Давление дифф. Аварийное макс.", xAccessorName: "pressureDeltaLimitMax", color: '#090' }, + { label: "Осевая нагрузка", xAccessorName: "axialLoad", color: '#009' }, + + { label: "Осевая нагрузка. Задание", xAccessorName: "axialLoadSp", color: '#600' }, + { label: "Осевая нагрузка. Аварийная макс.", xAccessorName: "axialLoadLimitMax", color: '#660' }, + { label: "Вес на крюке", xAccessorName: "hookWeight", color: '#606' }, + { label: "Вес на крюке. Холостой ход", xAccessorName: "hookWeightIdle", color: '#066' }, + { label: "Вес на крюке. Посадка", xAccessorName: "hookWeightLimitMin", color: '#060' }, + { label: "Вес на крюке. Затяжка", xAccessorName: "hookWeightLimitMax", color: '#006' }, + + { label: "Момент на роторе", xAccessorName: "rotorTorque", color: '#300' }, + { label: "Момент на роторе. Холостой ход", xAccessorName: "rotorTorqueIdle", color: '#330' }, + { label: "Момент на роторе. Задание", xAccessorName: "rotorTorqueSp", color: '#303' }, + { label: "Момент на роторе. Аварийный макс.", xAccessorName: "rotorTorqueLimitMax", color: '#033' }, + { label: "Обороты ротора", xAccessorName: "rotorSpeed", color: '#030' }, + { label: "Расход", xAccessorName: "flow", color: '#003' }, + + { label: "Расход. Холостой ход", xAccessorName: "flowIdle", color: '#666' }, + { label: "Расход. Аварийный макс.", xAccessorName: "flowDeltaLimitMax", color: '#ccc' }, +] + +const tagRender = ({ label, value, closable, onClose }) =>{ + const onPreventMouseDown = event => { + event.preventDefault(); + event.stopPropagation(); + }; + + let color = linesCollection.find(l=>l.xAccessorName === value)?.color + return ( + +       +  {label} + + ); +} + +export function ArchiveColumn({ data, config, rangeDate, chartRatio, onRemoveChart, onSaveConfig }) { + const [lines, setLines] = useState([]); + + useEffect(() => { + setLines(config.lines); + },[config]); + + const handleLinesSetChange = (linesKeys) => { + let newLines = linesCollection.filter(line => linesKeys.includes(line.xAccessorName)); + config.lines = newLines; + if(onSaveConfig) + onSaveConfig() + setLines(newLines); + }; + + let selectedValues = lines?.map(line=>line.xAccessorName)??[] + + const select = ; + + const popBar = + {select} + + + + + + return ( + <> + +
+ +
+
+ + ); +} diff --git a/src/components/WellTreeSelector.jsx b/src/components/WellTreeSelector.jsx index f8cf7f0..32e8531 100644 --- a/src/components/WellTreeSelector.jsx +++ b/src/components/WellTreeSelector.jsx @@ -1,4 +1,5 @@ import { useState, useEffect } from 'react' +import { useParams } from 'react-router-dom' import { WellService } from '../services/api' import Loader from '../components/Loader' import { TreeSelect } from 'antd' @@ -38,6 +39,7 @@ export default function WellTreeSelector(props) { const [wellsTree, setWellsTree] = useState([]) const [loader, setLoader] = useState(false) const history = useHistory() + let { id } = useParams(); let updateWellsList = async () => { setLoader(true) @@ -72,6 +74,7 @@ export default function WellTreeSelector(props) { treeData={wellsTree} treeDefaultExpandAll onSelect={onSelect} + value = {id} /> {loader && } diff --git a/src/components/charts/ChartTimeArchive.jsx b/src/components/charts/ChartTimeArchive.jsx new file mode 100644 index 0000000..042a236 --- /dev/null +++ b/src/components/charts/ChartTimeArchive.jsx @@ -0,0 +1,73 @@ +import moment from 'moment'; +import { useEffect, useState} from 'react'; +import {ChartTimeBase} from './ChartTimeBase' + +const GetRandomColor = () => "#" + Math.floor(Math.random()*16777215).toString(16) + +const CreateDataset = (lineConfig) => { + let color = lineConfig.borderColor + ?? lineConfig.backgroundColor + ?? lineConfig.color + ?? GetRandomColor() + + let dataset = { + label: lineConfig.label, + data: [], + backgroundColor: lineConfig.backgroundColor ?? color, + borderColor: lineConfig.borderColor ?? color, + borderWidth: lineConfig.borderWidth ?? 1, + borderDash: lineConfig.dash ?? [], + } + return dataset +} + +const ChartOptions = { + responsive: true, + // plugins:{ + // legend:{ + // maxHeight: 64, + // fullSize: true, + // display: true, + // posision:'chartArea', + // align: 'start', + // } + // } +} + +export const ChartTimeArchive = ({lines, data, yDisplay, rangeDate, chartRatio}) => { + const [dataParams, setDataParams] = useState({data:{datasets:[]}}) + + useEffect(() => { + if ((!lines) + || (!data)) + return + + let newDatasets = lines.map(lineCfg => { + let dataset = CreateDataset(lineCfg) + dataset.data = data.map(dataItem => { + return { + x: dataItem[lineCfg.xAccessorName], + y: new Date(dataItem[lineCfg.yAccessorName??'date']) + } + }) + return dataset + }) + + let interval = rangeDate ? (rangeDate[1] - rangeDate[0]) / 1000 : null + let startDate = rangeDate ? rangeDate[0] : moment() + let newParams = { + yInterval: interval, + yStart: startDate, + displayLabels: yDisplay??false, + data: { + datasets: newDatasets + } + } + setDataParams(newParams) + }, [data, lines, yDisplay, rangeDate, chartRatio]) + + const opt = ChartOptions + opt.aspectRatio = chartRatio + + return () +} diff --git a/src/components/charts/ChartTimeArchive.tsx b/src/components/charts/ChartTimeArchive.tsx deleted file mode 100644 index ec4fb6d..0000000 --- a/src/components/charts/ChartTimeArchive.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { useEffect, useState} from 'react'; -import {ChartTimeBase, ChartTimeData, ChartTimeDataParams} from './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 ?? [], - } - 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[] -} - -export type ChartTimeProps = { - label?: string, - yDisplay: Boolean, - lines: LineConfig[], - data: any[], - interval: number, -} - -export const ChartTimeArchive: 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: dataItem[lineCfg.xAccessorName], - y: new Date(dataItem[lineCfg.yAccessorName]) - } - }) - dataset.data = points; - }); - - 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]) - - return () -} \ No newline at end of file diff --git a/src/components/charts/ChartTimeBase.tsx b/src/components/charts/ChartTimeBase.tsx index 3ab6234..9fc607b 100644 --- a/src/components/charts/ChartTimeBase.tsx +++ b/src/components/charts/ChartTimeBase.tsx @@ -16,8 +16,9 @@ Chart.register( TimeScale, LinearScale, LineController, LineElement, PointElemen const defaultOptions = { //maintainAspectRatio: false, - aspectRatio:0.45, - animation: false, + aspectRatio: 0.45, + animation: false, + events: ['mousemove', 'mouseout', 'click', 'touchstart', 'touchmove'], scales: { y:{ type: 'time', @@ -38,12 +39,13 @@ const defaultOptions = { year: 'yyyy.MM', }, }, - position:{ x: 20 }, + //position:'center', grid:{ drawTicks: false, }, ticks: { z: 1, + display : false, textStrokeColor : "#ffff", textStrokeWidth : 2, color:"#000", @@ -98,49 +100,49 @@ const timeUnitByInterval = (intervalSec:number):String =>{ return 'year' } -export const ChartTimeBase: React.FC = (props) => { +export const ChartTimeBase: React.FC = ({options, dataParams}) => { const chartRef = useRef(null) - const [chart, setChart] = useState() useEffect(()=>{ - if(chartRef.current && (!chart)){ + if((chartRef.current)&&(!chart)){ let thisOptions = {} - Object.assign(thisOptions, defaultOptions, props.options) + Object.assign(thisOptions, defaultOptions, options) let newChart = new Chart(chartRef.current, { type: 'line', options: thisOptions, - data: props.dataParams.data + data: dataParams.data }) setChart(newChart) + return () => chart?.destroy() } - }, [chart, props.options, props.dataParams]) + }, [chart, options, dataParams]) useEffect(()=>{ if(!chart) return - chart.data = props.dataParams.data + chart.data = dataParams.data + chart.options.aspectRatio = options?.aspectRatio - if(props.dataParams.yStart){ - let interval = Number(props.dataParams.yInterval ?? 600) - let start = new Date(props.dataParams.yStart) - let end = new Date(props.dataParams.yStart) + if(dataParams.yStart){ + let interval = Number(dataParams.yInterval ?? 600) + let start = new Date(dataParams.yStart) + let end = new Date(dataParams.yStart) end.setSeconds(end.getSeconds() + interval) - if(chart.options.scales?.y){ 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.ticks.display = dataParams.displayLabels ?? true chart.options.scales.y.time.unit = timeUnitByInterval(interval) - chart.options.scales.y.time.stepSize = Math.round(interval/24) + chart.options.scales.y.time.stepSize = Math.round(interval/32) } } chart.update() - }, [chart, props]) + }, [chart, dataParams, options]) return() } \ No newline at end of file diff --git a/src/pages/Archive.jsx b/src/pages/Archive.jsx index 54285b5..036da4b 100644 --- a/src/pages/Archive.jsx +++ b/src/pages/Archive.jsx @@ -1,188 +1,139 @@ -import { useState, useEffect } from 'react' +import { useRef, useLayoutEffect, useState, useEffect } from 'react' import { Button, DatePicker, ConfigProvider, - Select, Row, Col, -} from 'antd' + Tooltip} from 'antd' import { useParams } from 'react-router-dom' -import { Subscribe } from '../services/signalr' import { DataService } from '../services/api' -import 'moment/locale/ru' import locale from 'antd/lib/locale/ru_RU' -import { ChartTimeArchive } from '../components/charts/ChartTimeArchive' -import { Display } from '../components/Display' -import { ChartTimeOnlineFooter } from '../components/ChartTimeOnlineFooter' -import { UserOfWells } from '../components/UserOfWells' - +import {generateUUID} from '../services/UidGenerator' +import { ArchiveColumn } from '../components/ArchiveColumn' +import moment from 'moment' const { RangePicker } = DatePicker; -const { Option } = Select; -// Выбор периода времени -const RangePickerLocalized = ({locale, ...other}) => { - return ( +const SaveObject = (key, obj) => { + let json = JSON.stringify(obj) + localStorage.setItem(key, json) +} + +const LoadObject = (key) => { + let json = localStorage.getItem(key) + return json ? JSON.parse(json) : null +} + +export default function Archive() { + let { id } = useParams(); + const [saubData, setSaubData] = useState([]) + const [chartsCfgs, setChartsCfgs] = useState([]) + const [rangeDate, setRangeDate] = useState([moment().subtract(3,'hours'), moment()]) + const [geometry, setGeometry] = useState({ratioRest:1, ratio1st:1, wRest:.5, w1st:.5}) + const chartsCfgsKey = 'chartsCfgs' + const chartsContainerRef = useRef(); + + const handleReceiveDataSaub = (data) => { + if (data) + setSaubData(data) + } + + const onAddChart = () => { + let newChartCfgs = [...chartsCfgs, {id: generateUUID(), yDisplay: false, aspectRatio:1}] + setChartsCfgs(newChartCfgs) + } + + const onRemoveChart = (id) => { + let newChartCfgs = chartsCfgs.filter(cfg => cfg.id !== id ) + setChartsCfgs(newChartCfgs) + } + + const onSaveConfig = ()=>{ + SaveObject(chartsCfgsKey, chartsCfgs) + } + + const onChangeRange = (range) => { + setRangeDate(range) + } + + useLayoutEffect(()=>{ + if(chartsContainerRef.current && chartsCfgs?.length){ + let w = chartsContainerRef.current.offsetWidth //1792 + w = w > 0 ? w : 1792 + let ot = chartsContainerRef.current.offsetTop + let ph = chartsContainerRef.current.offsetParent.offsetHeight + let h = ph - ot - 32 //761 + h = h > 0 ? h : 761 + + let chartsCount = chartsCfgs.length + + let labelLenght = 8 + let borderWidth = 8 + let wRest = Math.floor((w - labelLenght)/chartsCount) - borderWidth + let w1st = wRest + labelLenght + + let ratio1st = w1st/h + let ratioRest = wRest/h + + setGeometry({ratio1st, ratioRest, w1st, wRest}) + } + },[chartsContainerRef, chartsCfgs]) + + useEffect(() => { + let cfgs = LoadObject(chartsCfgsKey) + if(cfgs) + setChartsCfgs(cfgs) + },[]) + + useEffect(()=>{ + SaveObject(chartsCfgsKey, chartsCfgs) + },[chartsCfgs]) + + useEffect(() => { + let interval = (rangeDate[1] - rangeDate[0]) / 1000 + let startDate = rangeDate[0].toISOString() + + DataService.getData(id, startDate, interval, 2048) + .then(handleReceiveDataSaub) + .catch(error => console.error(error)) + }, [id, rangeDate]); + + let charts = null + if(chartsCfgs.length > 0){ + chartsCfgs[0].yDisplay = true + + charts = chartsCfgs.map((cfg, i) => + + + ) + } + + return (<> + + + + onChange = {onChangeRange} + value = {rangeDate} + /> - ) -} - -// Выбор "перьев" для графиков - перенести в шапку графика -const SelectDataCharts = () => { - const linesCollection = [ - { label: "Глубина забоя", xAccessorName: "wellDepth", color: '#a0a' }, - { label: "Положение инструмента", xAccessorName: "bitDepth", color: '#a0a' }, - { label: "Высота талевого блока", xAccessorName: "blockPosition", color: '#a0a' }, - { label: "Талевый блок. Мин положение", xAccessorName: "blockPositionMin", color: '#a0a' }, - { label: "Талевый блок. Макс положение", xAccessorName: "blockPositionMax", color: '#a0a' }, - { label: "Скорость талевого блока", xAccessorName: "blockSpeed", color: '#a0a' }, - { label: "Скорости талевого блока. Задание", xAccessorName: "blockSpeedSp", color: '#a0a' }, - { label: "Талевый блок. Задание скорости для роторного бурения", xAccessorName: "blockSpeedSpRotor", color: '#a0a' }, - { label: "Талевый блок. Задание скорости для режима слайда", xAccessorName: "blockSpeedSpSlide", color: '#a0a' }, - { label: "Талевый блок. Задание скорости для проработки", xAccessorName: "blockSpeedSpDevelop", color: '#a0a' }, - { label: "Давление", xAccessorName: "pressure", color: '#a0a' }, - { label: "Давление. Холостой ход", xAccessorName: "pressureIdle", color: '#a0a' }, - { label: "Давление. Задание", xAccessorName: "pressureSp", color: '#a0a' }, - { label: "Давление. Задание для роторного бурения", xAccessorName: "pressureSpRotor", color: '#a0a' }, - { label: "Давление. Задание для режима слайда", xAccessorName: "pressureSpSlide", color: '#a0a' }, - { label: "Давление. Задание для проработки", xAccessorName: "pressureSpDevelop", color: '#a0a' }, - { label: "Давление дифф. Аварийное макс.", xAccessorName: "pressureDeltaLimitMax", color: '#a0a' }, - { label: "Осевая нагрузка", xAccessorName: "axialLoad", color: '#a0a' }, - { label: "Осевая нагрузка. Задание", xAccessorName: "axialLoadSp", color: '#a0a' }, - { label: "Осевая нагрузка. Аварийная макс.", xAccessorName: "axialLoadLimitMax", color: '#a0a' }, - { label: "Вес на крюке", xAccessorName: "hookWeight", color: '#a0a' }, - { label: "Вес на крюке. Холостой ход", xAccessorName: "hookWeightIdle", color: '#a0a' }, - { label: "Вес на крюке. Посадка", xAccessorName: "hookWeightLimitMin", color: '#a0a' }, - { label: "Вес на крюке. Затяжка", xAccessorName: "hookWeightLimitMax", color: '#a0a' }, - { label: "Момент на роторе", xAccessorName: "rotorTorque", color: '#a0a' }, - { label: "Момент на роторе. Холостой ход", xAccessorName: "rotorTorqueIdle", color: '#a0a' }, - { label: "Момент на роторе. Задание", xAccessorName: "rotorTorqueSp", color: '#a0a' }, - { label: "Момент на роторе. Аварийный макс.", xAccessorName: "rotorTorqueLimitMax", color: '#a0a' }, - { label: "Обороты ротора", xAccessorName: "rotorSpeed", color: '#a0a' }, - { label: "Расход", xAccessorName: "flow", color: '#a0a' }, - { label: "Расход. Холостой ход", xAccessorName: "flowIdle", color: '#a0a' }, - { label: "Расход. Аварийный макс.", xAccessorName: "flowDeltaLimitMax", color: '#a0a' }, - ] - - const children = linesCollection.map((line) => ()) - - function handleChange(value) { - console.log(`selected ${value}`); - } - - return ( - - ) -} - -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 ( - <> - - - - ) -} - -const paramsGroups = [] - -export default function Archive(props) { - let { id } = useParams(); - const [saubData, setSaubData] = useState([]) - const [chartInterval, setChartInterval] = useState(600) - - const handleReceiveDataSaub = (data) => { - if (data) { - setSaubData(data) - } - } - - useEffect(() => { - DataService.getData(id) - .then(handleReceiveDataSaub) - .catch(error => console.error(error)) - - let unSubscribeMessages = Subscribe('ReceiveDataSaub', `well_${id}`, handleReceiveDataSaub) - return () => { - unSubscribeMessages() - } - }, [id]); - - const colSpan = 24 / (paramsGroups.length) - - return (<> -
-

Архив

- - -
-
- - - - - - Интервал:  - - - - - - - {paramsGroups.map(group => - - - )} - - + + {charts} ) } \ No newline at end of file diff --git a/src/services/UidGenerator.ts b/src/services/UidGenerator.ts new file mode 100644 index 0000000..4b58ec0 --- /dev/null +++ b/src/services/UidGenerator.ts @@ -0,0 +1,13 @@ +export function generateUUID():string { + let seed = (25869874412483 + * new Date().getTime() + * (performance && performance.now && (performance.now()))) % 173395562924509 + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + let random = Math.random() + seed = seed > 272 + ? seed = seed / 17 + : seed = (seed * random * random * 557833831325167) % 173395562924509 + random = Math.floor((random * seed) % 16) + return (c === 'x' ? random : (random & 0xB)).toString(16) + }) +} \ No newline at end of file