* Добавлена линия целевого значения операций

* Изменён метод отрисовки графика операций
* Добавлено обновление графика и таблицы при изменений целей
This commit is contained in:
goodmice 2022-06-16 17:14:52 +05:00
parent 5e4cb3cce4
commit 1fad389647
4 changed files with 76 additions and 35 deletions

View File

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

View File

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

View File

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

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