forked from ddrilling/asb_cloud_front
* Добавлена линия целевого значения операций
* Изменён метод отрисовки графика операций * Добавлено обновление графика и таблицы при изменений целей
This commit is contained in:
parent
5e4cb3cce4
commit
1fad389647
@ -1,24 +1,36 @@
|
||||
import { memo, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import * as d3 from 'd3'
|
||||
|
||||
import { makePointsOptimizator } from '@utils/functions/chart'
|
||||
|
||||
import '@styles/detected_operations.less'
|
||||
|
||||
export const OperationsChart = memo(({ data, yDomain, width, height, bottom = 30, left = 50, top = 10, right = 10, color = '#00F' }) => {
|
||||
const optimizePoints = makePointsOptimizator((a, b) => a.target === b.target)
|
||||
|
||||
export const OperationsChart = memo(({ data, yDomain, width, height, bottom = 30, left = 50, top = 10, right = 10, color = '#00F', targetColor = '#F00' }) => {
|
||||
const [ref, setRef] = useState(null)
|
||||
|
||||
const axisX = useRef(null)
|
||||
const axisY = useRef(null)
|
||||
const targetPath = useRef(null)
|
||||
const chartBars = useRef(null)
|
||||
|
||||
const w = useMemo(() => Number.isFinite(+width) ? +width : ref?.offsetWidth, [width, ref])
|
||||
const h = useMemo(() => Number.isFinite(+height) ? +height : ref?.offsetHeight, [height, ref])
|
||||
|
||||
const d = useMemo(() => data.map((row) => ({
|
||||
date: new Date(row.dateStart),
|
||||
value: row.durationMinutes,
|
||||
target: row.operationValue?.targetValue,
|
||||
})), [data]) // Нормализуем данные для графика
|
||||
|
||||
const x = useMemo(() => d3
|
||||
.scaleTime()
|
||||
.range([0, w - left - right])
|
||||
.domain([d3.min(d, d => d.date), d3.max(d, d => d.date)])
|
||||
.domain([
|
||||
d3.min(d, d => d.date),
|
||||
d3.max(d, d => d.date)
|
||||
])
|
||||
, [w, d, left, right]) // Создаём ось X
|
||||
|
||||
const y = useMemo(() => d3
|
||||
@ -27,25 +39,42 @@ export const OperationsChart = memo(({ data, yDomain, width, height, bottom = 30
|
||||
.domain([0, yDomain ?? d3.max(d, d => d.value)])
|
||||
, [h, d, top, bottom, yDomain]) // Создаём ось Y
|
||||
|
||||
const lines = useMemo(() => d.map(d => ({ x: x(d.date), y: y(d.value) })), [d, x, y]) // Получаем массив координат линий
|
||||
const targetLine = useMemo(() => d3.line()
|
||||
.x(d => x(d.date))
|
||||
.y(d => y(d.target))
|
||||
.defined(d => (d.target ?? null) !== null && !Number.isNaN(d.target))
|
||||
, [x, y])
|
||||
|
||||
useEffect(() => {
|
||||
d3.select(axisX.current).call(d3.axisBottom(x))
|
||||
}, [axisX, x]) // Рисуем ось X
|
||||
useEffect(() => { d3.select(targetPath.current).attr('d', targetLine(optimizePoints(d))) }, [d, targetLine, targetColor]) // Рисуем линию целевого значения
|
||||
useEffect(() => { d3.select(axisX.current).call(d3.axisBottom(x)) }, [axisX, x]) // Рисуем ось X
|
||||
useEffect(() => { d3.select(axisY.current).call(d3.axisLeft(y)) }, [axisY, y]) // Рисуем ось Y
|
||||
|
||||
useEffect(() => {
|
||||
d3.select(axisY.current).call(d3.axisLeft(y))
|
||||
}, [axisY, y]) // Рисуем ось Y
|
||||
useEffect(() => { // Рисуем столбики операций
|
||||
const bars = d3
|
||||
.select(chartBars.current)
|
||||
.selectAll('line')
|
||||
.data(d)
|
||||
|
||||
bars.exit().remove() // Удаляем лишние линии
|
||||
|
||||
bars.enter()
|
||||
.append('line') // Создаём новые линии, если не хватает
|
||||
.merge(bars) // Объединяем с существующими
|
||||
.attr('x1', d => x(d.date)) // Присваиваем значения
|
||||
.attr('x2', d => x(d.date))
|
||||
.attr('y1', h - bottom - top)
|
||||
.attr('y2', d => y(d.value))
|
||||
|
||||
}, [d, x, y, color, h, bottom, top])
|
||||
|
||||
return (
|
||||
<div className={'page-left'} ref={setRef}>
|
||||
<svg width={width ?? '100%'} height={height ?? '100%'}>
|
||||
<g ref={axisX} className={'axis x'} transform={`translate(${left}, ${h - bottom})`} />
|
||||
<g ref={axisY} className={'axis y'} transform={`translate(${left}, ${top})`} />
|
||||
<g transform={`translate(${left}, ${top})`} stroke={color}>
|
||||
{lines.map(({ x, y }, i) => (
|
||||
<line key={i} x1={x} y1={h - bottom - top} x2={x} y2={y} />
|
||||
))}
|
||||
<g transform={`translate(${left}, ${top})`}>
|
||||
<g ref={chartBars} stroke={color} />
|
||||
<path ref={targetPath} stroke={targetColor} fill={'none'} />
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
|
@ -15,7 +15,7 @@ const columnOptions = {
|
||||
const scroll = { y: '75vh', scrollToFirstRowOnChange: true }
|
||||
const numericRender = makeNumericRender(2)
|
||||
|
||||
export const TargetEditor = memo(({ loading }) => {
|
||||
export const TargetEditor = memo(({ loading, onChange }) => {
|
||||
const [targets, setTargets] = useState([])
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
const [showLoader, setShowLoader] = useState(false)
|
||||
@ -33,23 +33,23 @@ export const TargetEditor = memo(({ loading }) => {
|
||||
'Получение списка целей',
|
||||
), [idWell])
|
||||
|
||||
const onModalOpen = useCallback(() => {
|
||||
setShowModal(true)
|
||||
}, [])
|
||||
|
||||
const onModalCancel = useCallback(() => {
|
||||
setShowModal(false)
|
||||
}, [])
|
||||
const onModalOpen = useCallback(() => setShowModal(true), [])
|
||||
const onModalCancel = useCallback(() => setShowModal(false), [])
|
||||
|
||||
const recordParser = useCallback((record) => ({ ...record, idWell }), [idWell])
|
||||
|
||||
const onTargetChange = useCallback(() => {
|
||||
updateTable()
|
||||
onChange?.()
|
||||
}, [updateTable, onChange])
|
||||
|
||||
const isLoading = useMemo(() => loading || showLoader, [loading, showLoader])
|
||||
|
||||
const tableHandlers = useMemo(() => {
|
||||
const handlerProps = {
|
||||
service: OperationValueService,
|
||||
setLoader: setShowLoader,
|
||||
onComplete: updateTable,
|
||||
onComplete: onTargetChange,
|
||||
permission: 'OperationValue.edit',
|
||||
}
|
||||
|
||||
@ -58,7 +58,7 @@ export const TargetEditor = memo(({ loading }) => {
|
||||
edit: { ...handlerProps, action: 'update', actionName: 'Редактирование цели', recordParser },
|
||||
delete: { ...handlerProps, action: 'delete', actionName: 'Удаление цели', permission: 'OperationValue.delete' },
|
||||
}
|
||||
}, [updateTable, recordParser])
|
||||
}, [onTargetChange, recordParser])
|
||||
|
||||
useEffect(() => {
|
||||
invokeWebApiWrapperAsync(
|
||||
|
@ -48,6 +48,17 @@ const Operations = memo(() => {
|
||||
'Получение списка бурильщиков'
|
||||
), [])
|
||||
|
||||
const updateData = useCallback(async () => invokeWebApiWrapperAsync(
|
||||
async () => {
|
||||
if (!dates) return
|
||||
const data = await DetectedOperationService.get(idWell, undefined, dates[0].toISOString(), dates[1].toISOString())
|
||||
setData(data)
|
||||
},
|
||||
setIsLoading,
|
||||
'Не удалось загрузить список определённых операций',
|
||||
'Получение списка определённых операций',
|
||||
), [idWell, dates])
|
||||
|
||||
useEffect(() => {
|
||||
if (permissions.driller.get)
|
||||
updateDrillers()
|
||||
@ -70,17 +81,8 @@ const Operations = memo(() => {
|
||||
}, [idWell])
|
||||
|
||||
useEffect(() => {
|
||||
invokeWebApiWrapperAsync(
|
||||
async () => {
|
||||
if (!dates) return
|
||||
const data = await DetectedOperationService.get(idWell, undefined, dates[0].toISOString(), dates[1].toISOString())
|
||||
setData(data)
|
||||
},
|
||||
setIsLoading,
|
||||
'Не удалось загрузить список определённых операций',
|
||||
'Получение списка определённых операций',
|
||||
)
|
||||
}, [idWell, dates])
|
||||
updateData()
|
||||
}, [updateData])
|
||||
|
||||
return (
|
||||
<div className={'container detected-operations-page'}>
|
||||
@ -110,7 +112,7 @@ const Operations = memo(() => {
|
||||
</>
|
||||
)}
|
||||
{permissions.detectedOperation.get && permissions.operationValue.get && (
|
||||
<TargetEditor />
|
||||
<TargetEditor onChange={updateData} />
|
||||
)}
|
||||
</div>
|
||||
<LoaderPortal show={isLoading}>
|
||||
|
10
src/utils/functions/chart.ts
Normal file
10
src/utils/functions/chart.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export const makePointsOptimizator = <T,>(isEquals: (a: Record<string, T>, b: Record<string, T>) => boolean) => (points: Record<string, T>[]) => {
|
||||
if (!Array.isArray(points) || points.length < 3) return points
|
||||
|
||||
const out: Record<string, T>[] = []
|
||||
for (let i = 1; i < points.length - 1; i++)
|
||||
if (!isEquals(points[i - 1], points[i]) || !isEquals(points[i], points[i + 1]))
|
||||
out.push(points[i])
|
||||
|
||||
return [points[0], ...out, points[points.length - 1]]
|
||||
}
|
Loading…
Reference in New Issue
Block a user