Merge branch 'dev' into feature/bundle-optimization

This commit is contained in:
Александр Сироткин 2022-08-07 14:14:19 +05:00
commit 35c6da6c24
16 changed files with 517 additions and 92 deletions

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 () => {

View File

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

View 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

View 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

View 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
View 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',
})

View File

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