diff --git a/src/components/selectors/WellTreeSelector.tsx b/src/components/selectors/WellTreeSelector.tsx
index f12fa2d..f06b588 100755
--- a/src/components/selectors/WellTreeSelector.tsx
+++ b/src/components/selectors/WellTreeSelector.tsx
@@ -70,7 +70,6 @@ const getWellSortScore = (well: WellDto) => {
let out = [1, 2, 0][well.idState ?? 2]
const timeout = Date.now() - +new Date(well.lastTelemetryDate || 0)
if (timeout < 600_000) out += 600_000 - timeout
- console.log(well, out)
return out
}
diff --git a/src/pages/Analytics/WellCompositeEditor/WellCompositeSections.jsx b/src/pages/Analytics/WellCompositeEditor/WellCompositeSections.jsx
old mode 100755
new mode 100644
diff --git a/src/pages/Telemetry/Operations/index.jsx b/src/pages/Telemetry/Operations/index.jsx
index 2688873..8306be8 100644
--- a/src/pages/Telemetry/Operations/index.jsx
+++ b/src/pages/Telemetry/Operations/index.jsx
@@ -69,9 +69,17 @@ const Operations = memo(() => {
useEffect(() => {
invokeWebApiWrapperAsync(
async () => {
- const categories = arrayOrDefault(await DetectedOperationService.getCategories(idWell))
+ const categories = arrayOrDefault(await DetectedOperationService.getCategories())
setCategories(categories.map(({ id, name }) => ({ value: id, label: name })))
+ },
+ setIsLoading,
+ 'Не удалось загрзуить категории операций'
+ )
+ }, [])
+ useEffect(() => {
+ invokeWebApiWrapperAsync(
+ async () => {
const dates = await TelemetryDataSaubService.getDataDatesRange(idWell)
if (dates) {
const dt = [moment(dates.from), moment(dates.to)]
@@ -131,11 +139,11 @@ const Operations = memo(() => {
-
+
diff --git a/src/pages/Telemetry/TelemetryView/index.jsx b/src/pages/Telemetry/TelemetryView/index.jsx
index 5f28e42..7d5791c 100755
--- a/src/pages/Telemetry/TelemetryView/index.jsx
+++ b/src/pages/Telemetry/TelemetryView/index.jsx
@@ -1,6 +1,6 @@
-import { Select } from 'antd'
-import { BehaviorSubject, buffer, throttleTime } from 'rxjs'
import { useState, useEffect, useCallback, memo, useMemo } from 'react'
+import { BehaviorSubject, buffer, throttleTime } from 'rxjs'
+import { Select } from 'antd'
import { useIdWell } from '@asb/context'
import { makeDateSorter } from '@components/Table'
diff --git a/src/pages/WellOperations/Tvd/NetGraphExport.jsx b/src/pages/WellOperations/Tvd/NetGraphExport.jsx
index 72a5d72..048a024 100755
--- a/src/pages/WellOperations/Tvd/NetGraphExport.jsx
+++ b/src/pages/WellOperations/Tvd/NetGraphExport.jsx
@@ -14,15 +14,16 @@ export const NetGraphExport = memo(({ idWell, ...other }) => {
), [idWell])
return (
- }
- loading={isFileExporting}
- onClick={onExport}
- style={{ marginRight: '5px' }}
- {...other}
- >
- Сетевой график
-
+
+ }
+ loading={isFileExporting}
+ onClick={onExport}
+ {...other}
+ >
+ Сетевой график
+
+
)
})
diff --git a/src/pages/WellOperations/Tvd/StatExport.jsx b/src/pages/WellOperations/Tvd/StatExport.jsx
new file mode 100644
index 0000000..e425ecc
--- /dev/null
+++ b/src/pages/WellOperations/Tvd/StatExport.jsx
@@ -0,0 +1,38 @@
+import { memo, useCallback, useEffect, useState } from 'react'
+import { Button, Input } from 'antd'
+
+import { useIdWell } from '@asb/context'
+import { download, invokeWebApiWrapperAsync } from '@components/factory'
+import { WellService } from '@api'
+
+export const StatExport = memo(() => {
+ const [isFileExporting, setIsFileExporting] = useState(false)
+ const [idCluster, setIdCluster] = useState()
+ const idWell = useIdWell()
+
+ useEffect(() => {
+ invokeWebApiWrapperAsync(
+ async () => {
+ const { idCluster } = await WellService.get(idWell)
+ setIdCluster(idCluster)
+ },
+ setIsFileExporting,
+ 'Не удалось загрузить ID куста'
+ )
+ }, [idWell])
+
+ const onExport = useCallback((well) => invokeWebApiWrapperAsync(
+ async () => await download(`/api/DetectedOperation/export?${well ? 'idWell' : 'idCluster'}=${well ? idWell : idCluster}`),
+ setIsFileExporting,
+ 'Не удалось загрузить файл'
+ ), [idWell, idCluster])
+
+ return (
+
+
+
+
+ )
+})
+
+export default StatExport
diff --git a/src/pages/WellOperations/Tvd/TLChart.jsx b/src/pages/WellOperations/Tvd/TLChart.jsx
new file mode 100644
index 0000000..b2ed74b
--- /dev/null
+++ b/src/pages/WellOperations/Tvd/TLChart.jsx
@@ -0,0 +1,151 @@
+import { memo, useEffect, useMemo, useState } from 'react'
+import { useElementSize } from 'usehooks-ts'
+import { Empty } from 'antd'
+import moment from 'moment'
+import * as d3 from 'd3'
+
+import { useIdWell } from '@asb/context'
+import LoaderPortal from '@components/LoaderPortal'
+import { invokeWebApiWrapperAsync } from '@components/factory'
+import { DetectedOperationService } from '@api'
+import { formatDate } from '@utils'
+
+const defaultOffset = { left: 40, right: 20, top: 20, bottom: 20 }
+const zeroDate = moment('2000-01-01 00:00:00')
+
+const applyTime = (date) => moment(`${zeroDate.format('YYYY-MM-DD')} ${date.format('HH:mm:ss')}`)
+
+const splitByDate = (startTime, endTime) => {
+ if (startTime.isSame(endTime, 'day'))
+ return [{ startTime, endTime }]
+
+ const out = []
+ let date = moment(startTime).startOf('day').add(1, 'day')
+
+ out.push({ startTime, endTime: moment(date).subtract(1, 'ms') })
+
+ while(!date.isSame(endTime, 'day')) {
+ const newDate = moment(date).add(1, 'day')
+ out.push({ startTime: date, endTime: moment(newDate).subtract(1, 'ms') })
+ date = newDate
+ }
+
+ out.push({ startTime: date, endTime })
+
+ return out
+}
+
+export const TLChart = memo(({
+ backgroundColor = '#0000',
+ barHeight = 15,
+ offset = defaultOffset,
+ color,
+}) => {
+ const [isLoading, setIsLoading] = useState(false)
+ const [svgRef, setSvgRef] = useState()
+ const [data, setData] = useState()
+
+ const [rootRef, { width, height }] = useElementSize()
+
+ const idWell = useIdWell()
+
+ const dates = useMemo(() => {
+ if (!data || data.length <= 0) return [0, 0]
+ return [
+ d3.min(data, (d) => moment(d.dateStart)).startOf('day'),
+ d3.max(data, (d) => moment(d.dateEnd)).endOf('day'),
+ ]
+ }, [data])
+
+ const xAxis = useMemo(() => d3.scaleTime()
+ .range([0, width - offset.left - offset.right])
+ .domain([zeroDate, moment(zeroDate).endOf('day')])
+ , [width, offset])
+
+ const yAxis = useMemo(() => d3.scaleTime()
+ .range([0, height - offset.top - offset.bottom - barHeight])
+ .domain(dates)
+ , [height, offset, barHeight, dates])
+
+ useEffect(() => {
+ invokeWebApiWrapperAsync(
+ async () => {
+ const { operations } = await DetectedOperationService.get(idWell)
+ setData(operations.map((raw) => {
+ const startTime = moment(raw.dateStart)
+ const endTime = moment(raw.dateEnd)
+ return splitByDate(startTime, endTime).map((dt) => ({
+ ...raw,
+ startTime: dt.startTime,
+ endTime: dt.endTime,
+ }))
+ }).flat())
+ },
+ setIsLoading,
+ 'Не удалось загрузить список операций'
+ )
+ }, [idWell])
+
+ useEffect(() => { // Рисуем ось X
+ const xAxisArea = d3.select(svgRef).select('.axis.x')
+ xAxisArea.call(d3.axisTop(xAxis)
+ .tickSize(offset.top + offset.bottom - height)
+ .tickFormat((d) => formatDate(d, undefined, 'HH:mm:ss'))
+ .ticks(d3.timeHour.every(3))
+ )
+
+ xAxisArea.selectAll('.tick line')
+ .attr('stroke', 'black')
+ .attr('stroke-dasharray', [5, 3])
+ }, [svgRef, xAxis, height, offset])
+
+ useEffect(() => { // Рисуем ось Y
+ d3.select(svgRef)
+ .select('.axis.y')
+ .call(d3.axisLeft(yAxis)
+ .tickSize(0)
+ .ticks(d3.timeDay.every(1))
+ .tickFormat((d) => moment(d).format('DD.MM'))
+ )
+ }, [svgRef, yAxis, height, offset, dates])
+
+ useEffect(() => {
+ if (!data) return
+ const elms = d3.select(svgRef).select('.chart-area').selectAll('rect').data(data)
+ elms.exit().remove()
+ const newElms = elms.enter().append('rect')
+ elms.merge(newElms)
+ .attr('x', (d) => xAxis(applyTime(d.startTime)))
+ .attr('y', (d) => yAxis(moment(d.startTime).startOf('day')) - barHeight / 2)
+ .attr('width', (d) => xAxis(d.endTime) - xAxis(d.startTime))
+ .attr('height', barHeight)
+ .attr('fill', (d) => color ? color(d.idCategory) : '#0008')
+ }, [svgRef, xAxis, yAxis, data, color])
+
+ return (
+
+
+ {data ? (
+
+ ) : (
+
+
+
+ )}
+
+
+ )
+})
+
+export default TLChart
diff --git a/src/pages/WellOperations/Tvd/TLPie.jsx b/src/pages/WellOperations/Tvd/TLPie.jsx
new file mode 100644
index 0000000..1305ca5
--- /dev/null
+++ b/src/pages/WellOperations/Tvd/TLPie.jsx
@@ -0,0 +1,149 @@
+import { memo, useEffect, useMemo, useState } from 'react'
+import { useElementSize } from 'usehooks-ts'
+import { Empty } from 'antd'
+import * as d3 from 'd3'
+
+import { useIdWell } from '@asb/context'
+import { makeColumn, makeNumericColumn, makeTextColumn, Table } from '@components/Table'
+import LoaderPortal from '@components/LoaderPortal'
+import { invokeWebApiWrapperAsync } from '@components/factory'
+import { DetectedOperationService } from '@api'
+
+const tableColumns = [
+ makeColumn('Цвет', 'color', { width: 50, render: (d) => (
+
+ ) }),
+ makeTextColumn('Название', 'category', undefined, undefined, undefined, { width: 300 }),
+ makeNumericColumn('Время, мин', 'minutesTotal', undefined, undefined, undefined, 100),
+ makeNumericColumn('Кол-во', 'count', undefined, undefined, (d) => d ? d.toString() : '---', 100),
+ makeNumericColumn('Процент, %', 'percent', undefined, undefined, (d) => d ? d.toFixed(2) : '---', 100)
+]
+
+export const TLPie = memo(({ color }) => {
+ const [isLoading, setIsLoading] = useState(false)
+ const [svgRef, setSvgRef] = useState()
+ const [stats, setStats] = useState([])
+
+ const [rootRef, { width, height }] = useElementSize()
+
+ const idWell = useIdWell()
+
+ const pie = useMemo(() => d3.pie().value((d) => d.minutesTotal), [])
+
+ const tableData = useMemo(() => {
+ if (!stats) return null
+ const totalTime = stats.reduce((out, stat) => out + stat.minutesTotal, 0)
+ return stats.map((stat) => ({
+ ...stat,
+ color: color(stat.idCategory),
+ percent: stat.minutesTotal / totalTime * 100,
+ }))
+ }, [stats, color])
+
+ const data = useMemo(() => tableData ? pie(tableData) : null, [tableData])
+
+ const radius = useMemo(() => Math.min(width, height) / 2, [width, height])
+
+ useEffect(() => {
+ invokeWebApiWrapperAsync(
+ async () => {
+ const stats = await DetectedOperationService.getStat(idWell)
+ setStats(stats)
+ },
+ setIsLoading,
+ 'Не удалось загрузить статистику автоопределённых операций'
+ )
+ }, [idWell])
+
+ useEffect(() => {
+ if (!data) return
+ const slices = d3.select(svgRef)
+ .select('.slices')
+ .selectAll('path')
+ .data(data)
+
+ slices.exit().remove()
+ const newSlices = slices.enter().append('path')
+
+ slices.merge(newSlices)
+ .attr('d', d3.arc().innerRadius(radius * 0.4).outerRadius(radius * 0.8))
+ .attr('fill', (d) => color ? color(d.data.idCategory) : '#0008')
+ }, [svgRef, data, color, radius])
+
+ useEffect(() => {
+ if (!data) 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(data, (d) => d.data.category)
+
+ lines.exit().remove()
+ const newLines = lines.enter().append('polyline')
+
+ const abovePi = (d) => (d.startAngle + d.endAngle) / 2 < Math.PI
+
+ lines.merge(newLines)
+ .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(data, (d) => d.data.category)
+
+ 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')
+ .attr('width', radius * 0.4)
+ .text((d) => `${d.data.percent.toFixed(2)}% (${d.data.minutesTotal.toFixed(2)} мин)`)
+
+ }, [svgRef, data, radius])
+
+ return (
+
+
+ {data ? (
+
+ ) : (
+
+
+
+ )}
+
+
+ )
+})
+
+export default TLPie
diff --git a/src/pages/WellOperations/Tvd/index.jsx b/src/pages/WellOperations/Tvd/index.jsx
old mode 100755
new mode 100644
index 08de1b2..8b495c9
--- a/src/pages/WellOperations/Tvd/index.jsx
+++ b/src/pages/WellOperations/Tvd/index.jsx
@@ -1,22 +1,34 @@
-import { DoubleLeftOutlined, DoubleRightOutlined, LineChartOutlined, LinkOutlined } from '@ant-design/icons'
-import { memo, useState, useEffect, useCallback, useMemo } from 'react'
+import { LineChartOutlined, LinkOutlined } from '@ant-design/icons'
+import { memo, useState, useEffect, useMemo } from 'react'
+import { Switch, Segmented, Button } from 'antd'
import { Link } from 'react-router-dom'
-import { Switch, Button } from 'antd'
-import { timeDay } from 'd3'
+import * as d3 from 'd3'
import { useIdWell } from '@asb/context'
import { D3Chart } from '@components/d3'
import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory'
import { formatDate, fractionalSum, wrapPrivateComponent, getOperations } from '@utils'
+import { DetectedOperationService } from '@api'
+import TLPie from './TLPie'
+import TLChart from './TLChart'
import NptTable from './NptTable'
+import StatExport from './StatExport'
import NetGraphExport from './NetGraphExport'
import AdditionalTables from './AdditionalTables'
import '@styles/index.css'
import '@styles/tvd.less'
+const colorArray = [
+ '#1abc9c', '#16a085', '#2ecc71', '#27ae60', '#3498db',
+ '#2980b9', '#9b59b6', '#8e44ad', '#34495e', '#2c3e50',
+ '#f1c40f', '#f39c12', '#e67e22', '#d35400', '#e74c3c',
+ '#c0392b', '#ecf0f1', '#bdc3c7', '#95a5a6', '#7f8c8d',
+]
+
+const Item = ({ label, children, ...other }) => ({label}: {children}
)
const numericRender = (d) => d && Number.isFinite(+d) ? (+d).toFixed(2) : '-'
@@ -85,7 +97,7 @@ const ticks = {
date: {
x: {
visible: true,
- count: timeDay.every(1),
+ count: d3.timeDay.every(1),
format: (d, i) => i % 2 === 0 ? formatDate(d, undefined, 'YYYY-MM-DD') : '',
},
y: { visible: true },
@@ -134,17 +146,13 @@ const makeDataset = (key, label, color, width, radius, dash) => ({
const Tvd = memo(({ idWell: wellId, title, ...other }) => {
const [xLabel, setXLabel] = useState('day')
const [operations, setOperations] = useState({})
- const [tableVisible, setTableVisible] = useState(false)
const [isLoading, setIsLoading] = useState(false)
const [pointsEnabled, setPointsEnabled] = useState(true)
+ const [selectedTab, setSelectedTab] = useState('Скрыть')
+ const [color, setColor] = useState()
const idWellContext = useIdWell()
const idWell = useMemo(() => wellId ?? idWellContext, [wellId, idWellContext])
-
- const toogleTable = useCallback(() => {
- setOperations(pre => ({ ...pre }))
- setTableVisible(v => !v)
- }, [])
const chartData = useMemo(() => {
const withoutNpt = []
@@ -160,15 +168,6 @@ const Tvd = memo(({ idWell: wellId, title, ...other }) => {
return { ...operations, withoutNpt }
}, [operations])
-
- useEffect(() => {
- invokeWebApiWrapperAsync(
- async () => setOperations(await getOperations(idWell)),
- setIsLoading,
- `Не удалось загрузить операции по скважине "${idWell}"`,
- 'Получение списка опервций по скважине'
- )
- }, [idWell])
const datasets = useMemo(() => {
const radius = pointsEnabled ? 6 : 1
@@ -181,29 +180,58 @@ const Tvd = memo(({ idWell: wellId, title, ...other }) => {
]
}, [pointsEnabled])
+ useEffect(() => {
+ invokeWebApiWrapperAsync(
+ async () => setOperations(await getOperations(idWell)),
+ setIsLoading,
+ `Не удалось загрузить операции по скважине "${idWell}"`,
+ 'Получение списка опервций по скважине'
+ )
+ }, [idWell])
+
+ useEffect(() => {
+ invokeWebApiWrapperAsync(
+ async () => {
+ const cats = await DetectedOperationService.getCategories()
+ const color = d3.scaleOrdinal()
+ .domain(cats.map((cat) => cat.id))
+ .range(colorArray)
+ setColor(() => color)
+ },
+ undefined,
+ 'Не удалось получить список типов операций'
+ )
+ }, [])
+
return (
-
{title || 'График Глубина-день'}
-
-
setPointsEnabled(checked)}
- style={{ marginRight: 20 }}
- title={'Нажмите для переключения видимости засечек на графиках'}
- />
- setXLabel(checked ? 'date' : 'day')}
- style={{ marginRight: '20px' }}
- title={'Нажмите для переключения горизонтальной оси'}
- />
+
+
{title || 'График Глубина-день'}
+ -
+
+
+ -
+ setPointsEnabled(checked)}
+ title={'Нажмите для переключения видимости засечек на графиках'}
+ />
+
+
+
+
- : } onClick={toogleTable}>НПВ
+
@@ -221,7 +249,9 @@ const Tvd = memo(({ idWell: wellId, title, ...other }) => {
animDurationMs={0}
/>
- {tableVisible &&
}
+ {selectedTab === 'НПВ' &&
}
+ {selectedTab === 'ЕСО' &&
}
+ {selectedTab === 'Статистика' &&
}
@@ -229,7 +259,7 @@ const Tvd = memo(({ idWell: wellId, title, ...other }) => {
})
export default wrapPrivateComponent(Tvd, {
- requirements: [ 'OperationStat.get' ],
+ requirements: [ 'OperationStat.get', 'DetectedOperation.get' ],
title: 'TVD',
route: 'tvd',
})
diff --git a/src/styles/tvd.less b/src/styles/tvd.less
index c5c1356..08137fc 100755
--- a/src/styles/tvd.less
+++ b/src/styles/tvd.less
@@ -6,9 +6,24 @@
.tvd-top {
display: flex;
- align-items: baseline;
+ align-items: center;
justify-content: space-between;
margin-top: 20px;
+
+ .tvd-inputs {
+ display: flex;
+ align-items: center;
+
+ .tvd-input-group {
+ display: flex;
+ align-items: center;
+ margin: 0 15px;
+
+ & > span {
+ margin-right: 5px;
+ }
+ }
+ }
}
.tvd-main {