diff --git a/src/pages/Telemetry/Operations/OperationsChart.jsx b/src/pages/Telemetry/Operations/OperationsChart.jsx index 8345f18..396aac6 100644 --- a/src/pages/Telemetry/Operations/OperationsChart.jsx +++ b/src/pages/Telemetry/Operations/OperationsChart.jsx @@ -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 (
- - {lines.map(({ x, y }, i) => ( - - ))} + + +
diff --git a/src/pages/Telemetry/Operations/TargetEditor.jsx b/src/pages/Telemetry/Operations/TargetEditor.jsx index 415175d..3583edd 100644 --- a/src/pages/Telemetry/Operations/TargetEditor.jsx +++ b/src/pages/Telemetry/Operations/TargetEditor.jsx @@ -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( diff --git a/src/pages/Telemetry/Operations/index.jsx b/src/pages/Telemetry/Operations/index.jsx index 0287ff2..e1ed381 100644 --- a/src/pages/Telemetry/Operations/index.jsx +++ b/src/pages/Telemetry/Operations/index.jsx @@ -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 (
@@ -110,7 +112,7 @@ const Operations = memo(() => { )} {permissions.detectedOperation.get && permissions.operationValue.get && ( - + )}
diff --git a/src/utils/functions/chart.ts b/src/utils/functions/chart.ts new file mode 100644 index 0000000..4d76019 --- /dev/null +++ b/src/utils/functions/chart.ts @@ -0,0 +1,10 @@ +export const makePointsOptimizator = (isEquals: (a: Record, b: Record) => boolean) => (points: Record[]) => { + if (!Array.isArray(points) || points.length < 3) return points + + const out: Record[] = [] + 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]] +}