forked from ddrilling/asb_cloud_front
Добавлен черновик окна статистики использования уставок
This commit is contained in:
parent
0cbd9559f2
commit
259e2e4be8
@ -0,0 +1,270 @@
|
|||||||
|
import { Button, Card, Input, Modal, Radio, Table } from 'antd'
|
||||||
|
import { memo, useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
|
import { useElementSize } from 'usehooks-ts'
|
||||||
|
import moment from 'moment'
|
||||||
|
import * as d3 from 'd3'
|
||||||
|
|
||||||
|
import { useWell } from '@asb/context'
|
||||||
|
import { Grid, GridItem } from '@components/Grid'
|
||||||
|
import LoaderPortal from '@components/LoaderPortal'
|
||||||
|
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||||
|
import { DateRangeWrapper, makeColumn, makeNumericColumn } from '@components/Table'
|
||||||
|
import { LimitingParameterService } from '@api'
|
||||||
|
import { unique } from '@utils/filters'
|
||||||
|
|
||||||
|
import { makeGetColor } from '@pages/Well/WellOperations/Tvd'
|
||||||
|
|
||||||
|
import '@styles/limiting_parameter_statistics.less'
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
makeColumn('Цвет', 'color', { width: 50, render: (d) => (
|
||||||
|
<div style={{ backgroundColor: d, padding: '5px 0' }} />
|
||||||
|
) }),
|
||||||
|
makeNumericColumn('Уставка', 'idFeedRegulator', undefined, undefined, (value) => `Регулятор: ${value}`),
|
||||||
|
makeNumericColumn('Проходка, м', 'depth'),
|
||||||
|
makeNumericColumn('Общее время работы, мин', 'totalMinutes'),
|
||||||
|
]
|
||||||
|
|
||||||
|
export const LimitingParameterStatistics = memo(() => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const [data, setData] = useState([])
|
||||||
|
const [mode, setMode] = useState('depth')
|
||||||
|
const [depthFilter, setDepthFilter] = useState({ from: null, to: null })
|
||||||
|
const [dateFilter, setDateFilter] = useState([moment().subtract(1, 'day'), moment()])
|
||||||
|
|
||||||
|
const [svgRef, setSvgRef] = useState()
|
||||||
|
const [selectedRegulator, setSelectedRegulator] = useState(null)
|
||||||
|
|
||||||
|
const [rootRef, { width, height }] = useElementSize()
|
||||||
|
|
||||||
|
const [well] = useWell()
|
||||||
|
|
||||||
|
const onDepthChanged = useCallback((e, type) => {
|
||||||
|
setDepthFilter((prev) => ({ ...prev, [type]: e?.target?.value }))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const onRow = useCallback((record) => {
|
||||||
|
const out = {
|
||||||
|
onMouseEnter: () => {
|
||||||
|
setSelectedRegulator(record.idFeedRegulator)
|
||||||
|
d3.selectAll('.tl-pie-part')
|
||||||
|
.filter((d) => d.data.idFeedRegulator === record.idFeedRegulator)
|
||||||
|
.attr('transform', 'scale(1.05)')
|
||||||
|
},
|
||||||
|
onMouseLeave: () => {
|
||||||
|
setSelectedRegulator(null)
|
||||||
|
d3.selectAll('.tl-pie-part')
|
||||||
|
.filter((d) => d.data.idFeedRegulator === record.idFeedRegulator)
|
||||||
|
.attr('transform', 'scale(1)')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (record.idFeedRegulator === selectedRegulator)
|
||||||
|
out.style = { background: '#FAFAFA', fontSize: '16px', fontWeight: '600' }
|
||||||
|
return out
|
||||||
|
}, [selectedRegulator])
|
||||||
|
|
||||||
|
const onPieOver = useCallback(function (e, d) {
|
||||||
|
setSelectedRegulator(d.data.idFeedRegulator)
|
||||||
|
d3.select(this).attr('transform', 'scale(1.05)')
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const onPieOut = useCallback(function (e, d) {
|
||||||
|
setSelectedRegulator(null)
|
||||||
|
d3.select(this).attr('transform', 'scale(1)')
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const update = useCallback(() => {
|
||||||
|
invokeWebApiWrapperAsync(
|
||||||
|
async () => {
|
||||||
|
const data = await LimitingParameterService.getStat(well.id,
|
||||||
|
mode === 'time' ? dateFilter[0] : undefined,
|
||||||
|
mode === 'time' ? dateFilter[1] : undefined,
|
||||||
|
mode === 'depth' ? depthFilter.from : undefined,
|
||||||
|
mode === 'depth' ? depthFilter.to : undefined,
|
||||||
|
)
|
||||||
|
setData(data)
|
||||||
|
},
|
||||||
|
setIsLoading,
|
||||||
|
`Не удалось загрузить статистику использования уставок`,
|
||||||
|
{ actionName: `Загрузка статистики использования уставок`, well }
|
||||||
|
)
|
||||||
|
}, [well, mode, dateFilter, depthFilter])
|
||||||
|
|
||||||
|
const pie = useMemo(() => d3.pie().value((d) => d.totalMinutes), [])
|
||||||
|
|
||||||
|
const getColor = useMemo(() => makeGetColor(data?.map((row) => row.idFeedRegulator).filter(unique)), [data])
|
||||||
|
|
||||||
|
const tableData = useMemo(() => {
|
||||||
|
if (!data) return null
|
||||||
|
const totalTime = data.reduce((out, stat) => out + stat.totalMinutes, 0)
|
||||||
|
return data.map((stat) => ({
|
||||||
|
...stat,
|
||||||
|
color: getColor(stat.idFeedRegulator),
|
||||||
|
percent: stat.totalMinutes / totalTime * 100,
|
||||||
|
}))
|
||||||
|
}, [data, getColor])
|
||||||
|
|
||||||
|
const pieData = useMemo(() => tableData ? pie(tableData) : null, [tableData])
|
||||||
|
|
||||||
|
const radius = useMemo(() => Math.min(width, height) / 2, [width, height])
|
||||||
|
|
||||||
|
useEffect(update, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!pieData) return
|
||||||
|
const slices = d3.select(svgRef)
|
||||||
|
.select('.slices')
|
||||||
|
.selectAll('path')
|
||||||
|
.data(pieData)
|
||||||
|
|
||||||
|
slices.exit().remove()
|
||||||
|
const newSlices = slices.enter().append('path')
|
||||||
|
|
||||||
|
slices.merge(newSlices)
|
||||||
|
.attr('class', 'tl-pie-part')
|
||||||
|
.attr('d', d3.arc().innerRadius(radius * 0.4).outerRadius(radius * 0.8))
|
||||||
|
.attr('fill', (d) => d.data.color)
|
||||||
|
.attr('data-id', (d) => d.idFeedRegulator)
|
||||||
|
.on('mouseover', onPieOver)
|
||||||
|
.on('mouseout', onPieOut)
|
||||||
|
}, [svgRef, pieData, radius, onPieOver, onPieOut])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!pieData) return
|
||||||
|
const innerArc = d3.arc().innerRadius(radius * 0.4).outerRadius(radius * 0.8)
|
||||||
|
const outerArc = d3.arc().innerRadius(radius * 0.9).outerRadius(radius * 0.9)
|
||||||
|
|
||||||
|
const lines = d3.select(svgRef)
|
||||||
|
.select('.lines')
|
||||||
|
.selectAll('polyline')
|
||||||
|
.data(pieData, (d) => `Регулятор №${d.data.idFeedRegulator}`)
|
||||||
|
|
||||||
|
lines.exit().remove()
|
||||||
|
const newLines = lines.enter()
|
||||||
|
.append('polyline')
|
||||||
|
|
||||||
|
const abovePi = (d) => (d.startAngle + d.endAngle) / 2 < Math.PI
|
||||||
|
|
||||||
|
lines.merge(newLines)
|
||||||
|
.style('display', (d) => d.data.idFeedRegulator !== selectedRegulator ? 'none' : 'block')
|
||||||
|
.attr('points', (d) => {
|
||||||
|
const pos = outerArc.centroid(d)
|
||||||
|
pos[0] = radius * 0.95 * (abovePi(d) ? 1 : -1)
|
||||||
|
return [innerArc.centroid(d), outerArc.centroid(d), pos]
|
||||||
|
})
|
||||||
|
|
||||||
|
const lables = d3.select(svgRef)
|
||||||
|
.select('.labels')
|
||||||
|
.selectAll('text')
|
||||||
|
.data(pieData, (d) => `Регулятор №${d.data.idFeedRegulator}`)
|
||||||
|
|
||||||
|
lables.exit().remove()
|
||||||
|
const newLabels = lables.enter()
|
||||||
|
.append('text')
|
||||||
|
.attr('dy', '.35em')
|
||||||
|
|
||||||
|
lables.merge(newLabels)
|
||||||
|
.attr('transform', (d) => {
|
||||||
|
const pos = outerArc.centroid(d)
|
||||||
|
pos[0] = radius * 0.95 * (abovePi(d) ? 1 : -1)
|
||||||
|
return `translate(${pos})`
|
||||||
|
})
|
||||||
|
.style('text-anchor', (d) => abovePi(d) ? 'start' : 'end')
|
||||||
|
.style('display', (d) => d.data.idFeedRegulator !== selectedRegulator ? 'none' : 'block')
|
||||||
|
.attr('width', radius * 0.4)
|
||||||
|
.text((d) => `${d.data.percent.toFixed(2)}% (${d.data.totalMinutes.toFixed(2)} мин)`)
|
||||||
|
|
||||||
|
}, [svgRef, pieData, radius, selectedRegulator])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button onClick={() => setIsOpen(true)}>Статистика использования уставок</Button>
|
||||||
|
<Modal
|
||||||
|
centered
|
||||||
|
width={1024}
|
||||||
|
footer={false}
|
||||||
|
title={'Статистика использования уставок'}
|
||||||
|
onCancel={() => setIsOpen(false)}
|
||||||
|
open={isOpen}
|
||||||
|
>
|
||||||
|
<LoaderPortal show={isLoading}>
|
||||||
|
<div className={'filter-groups'}>
|
||||||
|
<Input.Group compact style={{ flex: 1 }}>
|
||||||
|
<Input
|
||||||
|
addonBefore={(
|
||||||
|
<Radio
|
||||||
|
checked={mode === 'depth'}
|
||||||
|
onChange={() => setMode('depth')}
|
||||||
|
>
|
||||||
|
По глубине
|
||||||
|
</Radio>
|
||||||
|
)}
|
||||||
|
allowClear
|
||||||
|
disabled={mode !== 'depth'}
|
||||||
|
prefix={'От'}
|
||||||
|
suffix={'м'}
|
||||||
|
style={{ width: 'calc(50% + 113px / 2)', textAlign: 'right' }}
|
||||||
|
onChange={(e) => onDepthChanged(e, 'from')}
|
||||||
|
value={depthFilter.from}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
allowClear
|
||||||
|
disabled={mode !== 'depth'}
|
||||||
|
prefix={'До'}
|
||||||
|
suffix={'м'}
|
||||||
|
style={{ width: 'calc(50% - 113px / 2)', textAlign: 'right' }}
|
||||||
|
onChange={(e) => onDepthChanged(e, 'to')}
|
||||||
|
value={depthFilter.to}
|
||||||
|
/>
|
||||||
|
</Input.Group>
|
||||||
|
<Input.Group compact style={{ flex: 1 }}>
|
||||||
|
<Input style={{ width: 128 }} addonBefore={(
|
||||||
|
<Radio
|
||||||
|
checked={mode === 'time'}
|
||||||
|
onChange={() => setMode('time')}
|
||||||
|
>
|
||||||
|
По времени
|
||||||
|
</Radio>
|
||||||
|
)}/>
|
||||||
|
<DateRangeWrapper
|
||||||
|
showTime
|
||||||
|
value={dateFilter}
|
||||||
|
disabled={mode !== 'time'}
|
||||||
|
onCalendarChange={setDateFilter}
|
||||||
|
disabledDate={(date) => date.isAfter(moment())}
|
||||||
|
/>
|
||||||
|
</Input.Group>
|
||||||
|
</div>
|
||||||
|
<Button onClick={update} style={{ marginTop: 10 }}>Обновить</Button>
|
||||||
|
<div className={'lps-pie-chart'} ref={rootRef}>
|
||||||
|
{data ? (
|
||||||
|
<svg ref={setSvgRef} width={'100%'} height={'100%'}>
|
||||||
|
<g transform={`translate(${width / 2}, ${height / 2})`}>
|
||||||
|
<g className={'slices'} stroke={'#0005'} />
|
||||||
|
<g className={'labels'} fill={'black'} />
|
||||||
|
<g className={'lines'} fill={'none'} stroke={'black'} />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<div className={'empty-wrapper'}>
|
||||||
|
<Empty />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className={'modal-label'}>Итоговая таблица по скважине</div>
|
||||||
|
<Table
|
||||||
|
bordered
|
||||||
|
size={'small'}
|
||||||
|
pagination={false}
|
||||||
|
dataSource={tableData}
|
||||||
|
columns={columns}
|
||||||
|
onRow={onRow}
|
||||||
|
/>
|
||||||
|
</LoaderPortal>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default LimitingParameterStatistics
|
@ -21,6 +21,7 @@ import {
|
|||||||
|
|
||||||
import { calcFlowData, makeChartGroups, yAxis } from './dataset'
|
import { calcFlowData, makeChartGroups, yAxis } from './dataset'
|
||||||
import { ADDITIVE_PAGES, cutData, DATA_COUNT, getLoadingInterval, makeDateTimeDisabled } from './archive_methods'
|
import { ADDITIVE_PAGES, cutData, DATA_COUNT, getLoadingInterval, makeDateTimeDisabled } from './archive_methods'
|
||||||
|
import LimitingParameterStatistics from './LimitingParameterStatistics'
|
||||||
import ActiveMessagesOnline from './ActiveMessagesOnline'
|
import ActiveMessagesOnline from './ActiveMessagesOnline'
|
||||||
import TelemetrySummary from './TelemetrySummary'
|
import TelemetrySummary from './TelemetrySummary'
|
||||||
import WirelineRunOut from './WirelineRunOut'
|
import WirelineRunOut from './WirelineRunOut'
|
||||||
@ -270,6 +271,7 @@ const TelemetryView = memo(() => {
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<Setpoints />
|
<Setpoints />
|
||||||
|
<LimitingParameterStatistics />
|
||||||
<WirelineRunOut />
|
<WirelineRunOut />
|
||||||
<div className={'icons'}>
|
<div className={'icons'}>
|
||||||
<img src={isTorqueStabEnabled(spinLast) ? MomentStabPicEnabled : MomentStabPicDisabled} alt={'TorqueMaster'} />
|
<img src={isTorqueStabEnabled(spinLast) ? MomentStabPicEnabled : MomentStabPicDisabled} alt={'TorqueMaster'} />
|
||||||
|
29
src/styles/limiting_parameter_statistics.less
Normal file
29
src/styles/limiting_parameter_statistics.less
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
.filter-groups {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
& .filter-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .date-filter {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-label {
|
||||||
|
width: 100%;
|
||||||
|
margin: 20px 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lps-pie-chart {
|
||||||
|
min-height: 30vh;
|
||||||
|
max-height: 50vh;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user