forked from ddrilling/asb_cloud_front
Merge branch 'dev' into feature/bundle-optimization
This commit is contained in:
commit
35c6da6c24
@ -3,6 +3,8 @@ import * as d3 from 'd3'
|
||||
import { ChartRegistry } from '@components/d3/types'
|
||||
import { makePointsOptimizator } from '@utils'
|
||||
|
||||
import { appendTransition } from './base'
|
||||
|
||||
export const renderArea = <DataType extends Record<string, unknown>>(
|
||||
xAxis: (value: any) => number,
|
||||
yAxis: (value: any) => number,
|
||||
@ -47,10 +49,8 @@ export const renderArea = <DataType extends Record<string, unknown>>(
|
||||
if (chart().selectAll('path').empty())
|
||||
chart().append('path')
|
||||
|
||||
chart().selectAll('path')
|
||||
.transition()
|
||||
.duration(chart.animDurationMs || 0)
|
||||
.attr('d', area(data as any))
|
||||
appendTransition(chart().selectAll('path'), chart)
|
||||
.attr('d', area(data))
|
||||
.attr('stroke-dasharray', chart.dash ? String(chart.dash) : null)
|
||||
.attr('fill', chart.areaColor ?? null)
|
||||
|
||||
|
12
src/components/d3/renders/base.ts
Normal file
12
src/components/d3/renders/base.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import * as d3 from 'd3'
|
||||
|
||||
import { ChartRegistry } from '../types'
|
||||
|
||||
export const appendTransition = <DataType, BaseType extends d3.BaseType, Datum, PElement extends d3.BaseType, PDatum>(
|
||||
elms: d3.Selection<BaseType, Datum, PElement, PDatum>,
|
||||
chart: ChartRegistry<DataType>
|
||||
): d3.Selection<BaseType, Datum, PElement, PDatum> => {
|
||||
if (chart.animDurationMs && chart.animDurationMs > 0)
|
||||
return elms.transition().duration(chart.animDurationMs) as any
|
||||
return elms
|
||||
}
|
@ -3,6 +3,8 @@ import * as d3 from 'd3'
|
||||
import { ChartRegistry } from '@components/d3/types'
|
||||
import { makePointsOptimizator } from '@utils'
|
||||
|
||||
import { appendTransition } from './base'
|
||||
|
||||
export const renderLine = <DataType extends Record<string, unknown>>(
|
||||
xAxis: (value: any) => number,
|
||||
yAxis: (value: any) => number,
|
||||
@ -35,11 +37,9 @@ export const renderLine = <DataType extends Record<string, unknown>>(
|
||||
if (chart().selectAll('path').empty())
|
||||
chart().append('path')
|
||||
|
||||
chart().selectAll('path')
|
||||
.transition()
|
||||
.duration(chart.animDurationMs ?? 0)
|
||||
appendTransition(chart().selectAll('path'), chart)
|
||||
.attr('d', line(data))
|
||||
.attr('stroke-dasharray', String(chart.dash ?? ''))
|
||||
.attr('stroke-dasharray', chart.dash ? String(chart.dash) : null)
|
||||
|
||||
return data
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { ChartOffset, ChartRegistry } from '@components/d3/types'
|
||||
|
||||
import { appendTransition } from './base'
|
||||
|
||||
export const renderNeedle = <DataType extends Record<string, unknown>>(
|
||||
xAxis: (value: d3.NumberValue) => number,
|
||||
yAxis: (value: d3.NumberValue) => number,
|
||||
@ -19,10 +21,7 @@ export const renderNeedle = <DataType extends Record<string, unknown>>(
|
||||
currentNeedles.exit().remove()
|
||||
currentNeedles.enter().append('line')
|
||||
|
||||
chart()
|
||||
.selectAll<SVGLineElement, DataType>('line')
|
||||
.transition()
|
||||
.duration(chart.animDurationMs ?? 0)
|
||||
appendTransition(chart().selectAll<SVGLineElement, DataType>('line'), chart)
|
||||
.attr('x1', (d) => xAxis(chart.x(d)))
|
||||
.attr('x2', (d) => xAxis(chart.x(d)))
|
||||
.attr('y1', height - offset.bottom - offset.top)
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { ChartRegistry, PointChartDataset } from '@components/d3/types'
|
||||
|
||||
import { appendTransition } from './base'
|
||||
|
||||
const defaultConfig: Required<Omit<PointChartDataset, 'type'>> = {
|
||||
radius: 3,
|
||||
shape: 'circle',
|
||||
@ -10,6 +12,14 @@ const defaultConfig: Required<Omit<PointChartDataset, 'type'>> = {
|
||||
fillOpacity: 1,
|
||||
}
|
||||
|
||||
const getPointsRoot = <DataType,>(chart: ChartRegistry<DataType>, embeded?: boolean): d3.Selection<SVGGElement, any, any, any> => {
|
||||
const root = chart()
|
||||
if (!embeded) return root
|
||||
if (root.select('.points').empty())
|
||||
root.append('g').attr('class', 'points')
|
||||
return root.select('.points')
|
||||
}
|
||||
|
||||
export const renderPoint = <DataType extends Record<string, unknown>>(
|
||||
xAxis: (value: any) => number,
|
||||
yAxis: (value: any) => number,
|
||||
@ -24,19 +34,7 @@ export const renderPoint = <DataType extends Record<string, unknown>>(
|
||||
config = { ...defaultConfig, ...chart }
|
||||
else return data
|
||||
|
||||
const getPointsRoot = (): d3.Selection<any, any, any, any> => {
|
||||
let root = chart()
|
||||
if (embeded) {
|
||||
if (root.select('.points').empty())
|
||||
root.append('g').attr('class', 'points')
|
||||
root = root.select('.points')
|
||||
}
|
||||
return root
|
||||
}
|
||||
|
||||
getPointsRoot()
|
||||
.transition()
|
||||
.duration(chart.animDurationMs ?? 0)
|
||||
appendTransition(getPointsRoot(chart, embeded), chart)
|
||||
.attr('stroke-width', config.strokeWidth)
|
||||
.attr('fill-opacity', config.fillOpacity)
|
||||
.attr('fill', config.fillColor)
|
||||
@ -45,14 +43,14 @@ export const renderPoint = <DataType extends Record<string, unknown>>(
|
||||
|
||||
const shape = ['hline', 'vline'].includes(config.shape) ? 'line' : config.shape
|
||||
|
||||
const currentPoints = getPointsRoot()
|
||||
const currentPoints = getPointsRoot(chart, embeded)
|
||||
.selectAll(shape)
|
||||
.data(data.filter(chart.y))
|
||||
|
||||
currentPoints.exit().remove()
|
||||
currentPoints.enter().append(shape)
|
||||
|
||||
const newPoints = getPointsRoot()
|
||||
const newPoints = getPointsRoot(chart, embeded)
|
||||
.selectAll<d3.BaseType, DataType>(shape)
|
||||
.transition()
|
||||
.duration(chart.animDurationMs ?? 0)
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { getByAccessor } from '@components/d3/functions'
|
||||
import { ChartRegistry } from '@components/d3/types'
|
||||
|
||||
import { appendTransition } from './base'
|
||||
|
||||
export const renderRectArea = <DataType extends Record<string, any>>(
|
||||
xAxis: (value: d3.NumberValue) => number,
|
||||
yAxis: (value: d3.NumberValue) => number,
|
||||
@ -28,8 +30,7 @@ export const renderRectArea = <DataType extends Record<string, any>>(
|
||||
rects.exit().remove()
|
||||
rects.enter().append('rect')
|
||||
|
||||
const actualRects = chart()
|
||||
.selectAll<SVGRectElement, Record<string, any>>('rect')
|
||||
appendTransition(chart().selectAll<SVGRectElement, Record<string, any>>('rect'), chart)
|
||||
.attr('x1', (d) => xAxis(xMin(d)))
|
||||
.attr('x2', (d) => xAxis(xMax(d)))
|
||||
.attr('y1', (d) => yAxis(yMin(d)))
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
0
src/pages/Analytics/WellCompositeEditor/WellCompositeSections.jsx
Executable file → Normal file
0
src/pages/Analytics/WellCompositeEditor/WellCompositeSections.jsx
Executable file → Normal file
@ -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(() => {
|
||||
<div className={'page-main'}>
|
||||
<OperationsChart
|
||||
category={categories?.[selectedCategory]}
|
||||
data={data.operations}
|
||||
data={data?.operations}
|
||||
height={'50vh'}
|
||||
yDomain={yDomain}
|
||||
/>
|
||||
<OperationsTable data={data.stats} height={'20vh'} />
|
||||
<OperationsTable data={data?.stats} height={'20vh'} />
|
||||
</div>
|
||||
</LoaderPortal>
|
||||
</div>
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Select } from 'antd'
|
||||
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'
|
||||
@ -73,7 +74,7 @@ export const makeChartGroups = (flowChart) => {
|
||||
maxYAccessor: accessor + 'Max',
|
||||
bindDomainFrom: accessor,
|
||||
})
|
||||
console.log(flowChart)
|
||||
|
||||
return [
|
||||
[
|
||||
makeDataset('Высота блока', 'Высота ТБ','#303030', 'blockPosition', 'м'),
|
||||
@ -151,6 +152,9 @@ const TelemetryView = memo(() => {
|
||||
|
||||
const idWell = useIdWell()
|
||||
|
||||
const saubSubject$ = useMemo(() => new BehaviorSubject(), [])
|
||||
const spinSubject$ = useMemo(() => new BehaviorSubject(), [])
|
||||
|
||||
const handleDataSaub = useCallback((data) => {
|
||||
if (data) {
|
||||
const dataSaub = normalizeData(data)
|
||||
@ -165,11 +169,22 @@ const TelemetryView = memo(() => {
|
||||
const handleDataSpin = useCallback((data) => data && setDataSpin((prev) => [...prev, ...data]), [])
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = Subscribe(
|
||||
'hubs/telemetry', `well_${idWell}`,
|
||||
{ methodName: 'ReceiveDataSaub', handler: handleDataSaub },
|
||||
{ methodName: 'ReceiveDataSpin', handler: handleDataSpin }
|
||||
)
|
||||
const subscribtion = saubSubject$.pipe(
|
||||
buffer(saubSubject$.pipe(throttleTime(700)))
|
||||
).subscribe((data) => handleDataSaub(data.flat()))
|
||||
|
||||
return () => subscribtion.unsubscribe()
|
||||
}, [saubSubject$])
|
||||
|
||||
useEffect(() => {
|
||||
const subscribtion = spinSubject$.pipe(
|
||||
buffer(spinSubject$.pipe(throttleTime(700)))
|
||||
).subscribe((data) => handleDataSpin(data.flat()))
|
||||
|
||||
return () => subscribtion.unsubscribe()
|
||||
}, [spinSubject$])
|
||||
|
||||
useEffect(() => {
|
||||
invokeWebApiWrapperAsync(
|
||||
async () => {
|
||||
const flowChart = await DrillFlowChartService.getByIdWell(idWell)
|
||||
@ -183,9 +198,18 @@ const TelemetryView = memo(() => {
|
||||
`Не удалось получить данные по скважине "${idWell}"`,
|
||||
'Получение данных по скважине'
|
||||
)
|
||||
return unsubscribe
|
||||
}, [idWell, chartInterval, handleDataSpin, handleDataSaub])
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = Subscribe(
|
||||
'hubs/telemetry', `well_${idWell}`,
|
||||
{ methodName: 'ReceiveDataSaub', handler: (data) => saubSubject$.next(data) },
|
||||
{ methodName: 'ReceiveDataSpin', handler: (data) => spinSubject$.next(data) }
|
||||
)
|
||||
|
||||
return () => unsubscribe()
|
||||
}, [idWell, saubSubject$, spinSubject$])
|
||||
|
||||
useEffect(() => {
|
||||
invokeWebApiWrapperAsync(
|
||||
async () => {
|
||||
|
@ -14,15 +14,16 @@ export const NetGraphExport = memo(({ idWell, ...other }) => {
|
||||
), [idWell])
|
||||
|
||||
return (
|
||||
<Button
|
||||
icon={<ExportOutlined />}
|
||||
loading={isFileExporting}
|
||||
onClick={onExport}
|
||||
style={{ marginRight: '5px' }}
|
||||
{...other}
|
||||
>
|
||||
Сетевой график
|
||||
</Button>
|
||||
<div className={'tvd-input-group'}>
|
||||
<Button
|
||||
icon={<ExportOutlined />}
|
||||
loading={isFileExporting}
|
||||
onClick={onExport}
|
||||
{...other}
|
||||
>
|
||||
Сетевой график
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
|
38
src/pages/WellOperations/Tvd/StatExport.jsx
Normal file
38
src/pages/WellOperations/Tvd/StatExport.jsx
Normal file
@ -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 (
|
||||
<Input.Group compact>
|
||||
<Button loading={isFileExporting} onClick={() => onExport(false)}>Выгрузка (куст)</Button>
|
||||
<Button loading={isFileExporting} onClick={() => onExport(true)}>Выгрузка (скважина)</Button>
|
||||
</Input.Group>
|
||||
)
|
||||
})
|
||||
|
||||
export default StatExport
|
151
src/pages/WellOperations/Tvd/TLChart.jsx
Normal file
151
src/pages/WellOperations/Tvd/TLChart.jsx
Normal file
@ -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 (
|
||||
<div className={'tvd-right'} ref={rootRef}>
|
||||
<LoaderPortal show={isLoading} style={{ width: '100%', flexGrow: 1 }}>
|
||||
{data ? (
|
||||
<svg ref={setSvgRef} width={'100%'} height={'100%'}>
|
||||
<g className={'axis x'} transform={`translate(${offset.left}, ${offset.top})`} />
|
||||
<g className={'axis y'} transform={`translate(${offset.left}, ${offset.top + barHeight})`} />
|
||||
<g className={'chart-area'} transform={`translate(${offset.left}, ${offset.top + barHeight})`} stroke={'none'} />
|
||||
<rect
|
||||
x={offset.left}
|
||||
y={offset.top}
|
||||
width={Math.max(width - offset.left - offset.right, 0)}
|
||||
height={Math.max(height - offset.top - offset.bottom, 0)}
|
||||
fill={backgroundColor}
|
||||
/>
|
||||
</svg>
|
||||
) : (
|
||||
<div style={{ height: '100%', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
|
||||
<Empty />
|
||||
</div>
|
||||
)}
|
||||
</LoaderPortal>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
export default TLChart
|
149
src/pages/WellOperations/Tvd/TLPie.jsx
Normal file
149
src/pages/WellOperations/Tvd/TLPie.jsx
Normal file
@ -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) => (
|
||||
<div style={{ backgroundColor: d, padding: '5px 0' }} />
|
||||
) }),
|
||||
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 (
|
||||
<div className={'tvd-right'}>
|
||||
<LoaderPortal show={isLoading} style={{ width: '100%', flexGrow: 1 }}>
|
||||
{data ? (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'stretch', justifyContent: 'space-between', height: '100%' }}>
|
||||
<div ref={rootRef} style={{ flexGrow: 1 }}>
|
||||
<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>
|
||||
<Table
|
||||
size={'small'}
|
||||
columns={tableColumns}
|
||||
dataSource={tableData}
|
||||
scroll={{ y: '20vh', x: true }}
|
||||
pagination={false}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ height: '100%', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
|
||||
<Empty />
|
||||
</div>
|
||||
)}
|
||||
</LoaderPortal>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
export default TLPie
|
112
src/pages/WellOperations/Tvd/index.jsx
Executable file → Normal file
112
src/pages/WellOperations/Tvd/index.jsx
Executable file → Normal file
@ -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 }) => (<div className={'tvd-input-group'} {...other}><span>{label}: </span>{children}</div>)
|
||||
|
||||
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 (
|
||||
<div className={'container tvd-page'} {...other}>
|
||||
<div className={'tvd-top'}>
|
||||
<h2>{title || 'График Глубина-день'}</h2>
|
||||
<div>
|
||||
<Switch
|
||||
defaultChecked
|
||||
checkedChildren={'С рисками'}
|
||||
unCheckedChildren={'Без рисок'}
|
||||
onChange={(checked) => setPointsEnabled(checked)}
|
||||
style={{ marginRight: 20 }}
|
||||
title={'Нажмите для переключения видимости засечек на графиках'}
|
||||
/>
|
||||
<Switch
|
||||
checkedChildren={'Дата'}
|
||||
unCheckedChildren={'Дни со старта'}
|
||||
loading={isLoading}
|
||||
onChange={(checked) => setXLabel(checked ? 'date' : 'day')}
|
||||
style={{ marginRight: '20px' }}
|
||||
title={'Нажмите для переключения горизонтальной оси'}
|
||||
/>
|
||||
<div className={'tvd-inputs'}>
|
||||
<h2>{title || 'График Глубина-день'}</h2>
|
||||
<Item label={'Ось времени'} style={{ marginLeft: 50 }}>
|
||||
<Segmented
|
||||
options={[{ label: 'Дата', value: 'date' }, { label: 'Дни со старта', value: 'day' }]}
|
||||
onChange={setXLabel}
|
||||
value={xLabel}
|
||||
title={'Нажмите для переключения горизонтальной оси'}
|
||||
/>
|
||||
</Item>
|
||||
<Item label={'Риски'}>
|
||||
<Switch
|
||||
defaultChecked
|
||||
onChange={(checked) => setPointsEnabled(checked)}
|
||||
title={'Нажмите для переключения видимости засечек на графиках'}
|
||||
/>
|
||||
</Item>
|
||||
</div>
|
||||
<div className={'tvd-inputs'}>
|
||||
<StatExport />
|
||||
<NetGraphExport idWell={idWell} />
|
||||
<Button icon={tableVisible ? <DoubleRightOutlined /> : <DoubleLeftOutlined />} onClick={toogleTable}>НПВ</Button>
|
||||
<Segmented
|
||||
options={['НПВ', 'ЕСО', 'Статистика', 'Скрыть']}
|
||||
value={selectedTab}
|
||||
onChange={setSelectedTab}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<LoaderPortal show={isLoading} style={{ flex: 1 }}>
|
||||
@ -221,7 +249,9 @@ const Tvd = memo(({ idWell: wellId, title, ...other }) => {
|
||||
animDurationMs={0}
|
||||
/>
|
||||
</div>
|
||||
{tableVisible && <NptTable operations={operations?.fact} />}
|
||||
{selectedTab === 'НПВ' && <NptTable operations={operations?.fact} />}
|
||||
{selectedTab === 'ЕСО' && <TLChart color={color} />}
|
||||
{selectedTab === 'Статистика' && <TLPie color={color} />}
|
||||
</div>
|
||||
</LoaderPortal>
|
||||
</div>
|
||||
@ -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',
|
||||
})
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user