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 { memo, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import * as d3 from 'd3'
|
import * as d3 from 'd3'
|
||||||
|
|
||||||
|
import { makePointsOptimizator } from '@utils/functions/chart'
|
||||||
|
|
||||||
import '@styles/detected_operations.less'
|
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 [ref, setRef] = useState(null)
|
||||||
|
|
||||||
const axisX = useRef(null)
|
const axisX = useRef(null)
|
||||||
const axisY = 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 w = useMemo(() => Number.isFinite(+width) ? +width : ref?.offsetWidth, [width, ref])
|
||||||
const h = useMemo(() => Number.isFinite(+height) ? +height : ref?.offsetHeight, [height, ref])
|
const h = useMemo(() => Number.isFinite(+height) ? +height : ref?.offsetHeight, [height, ref])
|
||||||
|
|
||||||
const d = useMemo(() => data.map((row) => ({
|
const d = useMemo(() => data.map((row) => ({
|
||||||
date: new Date(row.dateStart),
|
date: new Date(row.dateStart),
|
||||||
value: row.durationMinutes,
|
value: row.durationMinutes,
|
||||||
|
target: row.operationValue?.targetValue,
|
||||||
})), [data]) // Нормализуем данные для графика
|
})), [data]) // Нормализуем данные для графика
|
||||||
|
|
||||||
const x = useMemo(() => d3
|
const x = useMemo(() => d3
|
||||||
.scaleTime()
|
.scaleTime()
|
||||||
.range([0, w - left - right])
|
.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
|
, [w, d, left, right]) // Создаём ось X
|
||||||
|
|
||||||
const y = useMemo(() => d3
|
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)])
|
.domain([0, yDomain ?? d3.max(d, d => d.value)])
|
||||||
, [h, d, top, bottom, yDomain]) // Создаём ось Y
|
, [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(() => {
|
useEffect(() => { d3.select(targetPath.current).attr('d', targetLine(optimizePoints(d))) }, [d, targetLine, targetColor]) // Рисуем линию целевого значения
|
||||||
d3.select(axisX.current).call(d3.axisBottom(x))
|
useEffect(() => { d3.select(axisX.current).call(d3.axisBottom(x)) }, [axisX, x]) // Рисуем ось X
|
||||||
}, [axisX, x]) // Рисуем ось X
|
useEffect(() => { d3.select(axisY.current).call(d3.axisLeft(y)) }, [axisY, y]) // Рисуем ось Y
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => { // Рисуем столбики операций
|
||||||
d3.select(axisY.current).call(d3.axisLeft(y))
|
const bars = d3
|
||||||
}, [axisY, y]) // Рисуем ось Y
|
.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 (
|
return (
|
||||||
<div className={'page-left'} ref={setRef}>
|
<div className={'page-left'} ref={setRef}>
|
||||||
<svg width={width ?? '100%'} height={height ?? '100%'}>
|
<svg width={width ?? '100%'} height={height ?? '100%'}>
|
||||||
<g ref={axisX} className={'axis x'} transform={`translate(${left}, ${h - bottom})`} />
|
<g ref={axisX} className={'axis x'} transform={`translate(${left}, ${h - bottom})`} />
|
||||||
<g ref={axisY} className={'axis y'} transform={`translate(${left}, ${top})`} />
|
<g ref={axisY} className={'axis y'} transform={`translate(${left}, ${top})`} />
|
||||||
<g transform={`translate(${left}, ${top})`} stroke={color}>
|
<g transform={`translate(${left}, ${top})`}>
|
||||||
{lines.map(({ x, y }, i) => (
|
<g ref={chartBars} stroke={color} />
|
||||||
<line key={i} x1={x} y1={h - bottom - top} x2={x} y2={y} />
|
<path ref={targetPath} stroke={targetColor} fill={'none'} />
|
||||||
))}
|
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
@ -15,7 +15,7 @@ const columnOptions = {
|
|||||||
const scroll = { y: '75vh', scrollToFirstRowOnChange: true }
|
const scroll = { y: '75vh', scrollToFirstRowOnChange: true }
|
||||||
const numericRender = makeNumericRender(2)
|
const numericRender = makeNumericRender(2)
|
||||||
|
|
||||||
export const TargetEditor = memo(({ loading }) => {
|
export const TargetEditor = memo(({ loading, onChange }) => {
|
||||||
const [targets, setTargets] = useState([])
|
const [targets, setTargets] = useState([])
|
||||||
const [showModal, setShowModal] = useState(false)
|
const [showModal, setShowModal] = useState(false)
|
||||||
const [showLoader, setShowLoader] = useState(false)
|
const [showLoader, setShowLoader] = useState(false)
|
||||||
@ -33,23 +33,23 @@ export const TargetEditor = memo(({ loading }) => {
|
|||||||
'Получение списка целей',
|
'Получение списка целей',
|
||||||
), [idWell])
|
), [idWell])
|
||||||
|
|
||||||
const onModalOpen = useCallback(() => {
|
const onModalOpen = useCallback(() => setShowModal(true), [])
|
||||||
setShowModal(true)
|
const onModalCancel = useCallback(() => setShowModal(false), [])
|
||||||
}, [])
|
|
||||||
|
|
||||||
const onModalCancel = useCallback(() => {
|
|
||||||
setShowModal(false)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const recordParser = useCallback((record) => ({ ...record, idWell }), [idWell])
|
const recordParser = useCallback((record) => ({ ...record, idWell }), [idWell])
|
||||||
|
|
||||||
|
const onTargetChange = useCallback(() => {
|
||||||
|
updateTable()
|
||||||
|
onChange?.()
|
||||||
|
}, [updateTable, onChange])
|
||||||
|
|
||||||
const isLoading = useMemo(() => loading || showLoader, [loading, showLoader])
|
const isLoading = useMemo(() => loading || showLoader, [loading, showLoader])
|
||||||
|
|
||||||
const tableHandlers = useMemo(() => {
|
const tableHandlers = useMemo(() => {
|
||||||
const handlerProps = {
|
const handlerProps = {
|
||||||
service: OperationValueService,
|
service: OperationValueService,
|
||||||
setLoader: setShowLoader,
|
setLoader: setShowLoader,
|
||||||
onComplete: updateTable,
|
onComplete: onTargetChange,
|
||||||
permission: 'OperationValue.edit',
|
permission: 'OperationValue.edit',
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,7 +58,7 @@ export const TargetEditor = memo(({ loading }) => {
|
|||||||
edit: { ...handlerProps, action: 'update', actionName: 'Редактирование цели', recordParser },
|
edit: { ...handlerProps, action: 'update', actionName: 'Редактирование цели', recordParser },
|
||||||
delete: { ...handlerProps, action: 'delete', actionName: 'Удаление цели', permission: 'OperationValue.delete' },
|
delete: { ...handlerProps, action: 'delete', actionName: 'Удаление цели', permission: 'OperationValue.delete' },
|
||||||
}
|
}
|
||||||
}, [updateTable, recordParser])
|
}, [onTargetChange, recordParser])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
invokeWebApiWrapperAsync(
|
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(() => {
|
useEffect(() => {
|
||||||
if (permissions.driller.get)
|
if (permissions.driller.get)
|
||||||
updateDrillers()
|
updateDrillers()
|
||||||
@ -70,17 +81,8 @@ const Operations = memo(() => {
|
|||||||
}, [idWell])
|
}, [idWell])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
invokeWebApiWrapperAsync(
|
updateData()
|
||||||
async () => {
|
}, [updateData])
|
||||||
if (!dates) return
|
|
||||||
const data = await DetectedOperationService.get(idWell, undefined, dates[0].toISOString(), dates[1].toISOString())
|
|
||||||
setData(data)
|
|
||||||
},
|
|
||||||
setIsLoading,
|
|
||||||
'Не удалось загрузить список определённых операций',
|
|
||||||
'Получение списка определённых операций',
|
|
||||||
)
|
|
||||||
}, [idWell, dates])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'container detected-operations-page'}>
|
<div className={'container detected-operations-page'}>
|
||||||
@ -110,7 +112,7 @@ const Operations = memo(() => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{permissions.detectedOperation.get && permissions.operationValue.get && (
|
{permissions.detectedOperation.get && permissions.operationValue.get && (
|
||||||
<TargetEditor />
|
<TargetEditor onChange={updateData} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<LoaderPortal show={isLoading}>
|
<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