Добавлен черновик окна статистики использования уставок

This commit is contained in:
Александр Сироткин 2022-11-28 10:13:40 +05:00
parent 0cbd9559f2
commit 259e2e4be8
3 changed files with 301 additions and 0 deletions

View File

@ -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

View File

@ -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'} />

View 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;
}