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 { ADDITIVE_PAGES, cutData, DATA_COUNT, getLoadingInterval, makeDateTimeDisabled } from './archive_methods'
|
||||
import LimitingParameterStatistics from './LimitingParameterStatistics'
|
||||
import ActiveMessagesOnline from './ActiveMessagesOnline'
|
||||
import TelemetrySummary from './TelemetrySummary'
|
||||
import WirelineRunOut from './WirelineRunOut'
|
||||
@ -270,6 +271,7 @@ const TelemetryView = memo(() => {
|
||||
</Select>
|
||||
</div>
|
||||
<Setpoints />
|
||||
<LimitingParameterStatistics />
|
||||
<WirelineRunOut />
|
||||
<div className={'icons'}>
|
||||
<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