diff --git a/src/pages/Well/Telemetry/TelemetryView/LimitingParameterStatistics/index.jsx b/src/pages/Well/Telemetry/TelemetryView/LimitingParameterStatistics/index.jsx new file mode 100644 index 0000000..50a9328 --- /dev/null +++ b/src/pages/Well/Telemetry/TelemetryView/LimitingParameterStatistics/index.jsx @@ -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) => ( +
+ ) }), + 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 ( + <> + + setIsOpen(false)} + open={isOpen} + > + +
+ + setMode('depth')} + > + По глубине + + )} + allowClear + disabled={mode !== 'depth'} + prefix={'От'} + suffix={'м'} + style={{ width: 'calc(50% + 113px / 2)', textAlign: 'right' }} + onChange={(e) => onDepthChanged(e, 'from')} + value={depthFilter.from} + /> + onDepthChanged(e, 'to')} + value={depthFilter.to} + /> + + + setMode('time')} + > + По времени + + )}/> + date.isAfter(moment())} + /> + +
+ +
+ {data ? ( + + + + + + + + ) : ( +
+ +
+ )} +
+
Итоговая таблица по скважине
+ + + + + ) +}) + +export default LimitingParameterStatistics diff --git a/src/pages/Well/Telemetry/TelemetryView/index.jsx b/src/pages/Well/Telemetry/TelemetryView/index.jsx index ee21a85..4bc9fbb 100644 --- a/src/pages/Well/Telemetry/TelemetryView/index.jsx +++ b/src/pages/Well/Telemetry/TelemetryView/index.jsx @@ -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(() => { +
{'TorqueMaster'} diff --git a/src/styles/limiting_parameter_statistics.less b/src/styles/limiting_parameter_statistics.less new file mode 100644 index 0000000..104a2d4 --- /dev/null +++ b/src/styles/limiting_parameter_statistics.less @@ -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; +} \ No newline at end of file