diff --git a/src/components/Table/Columns/index.ts b/src/components/Table/Columns/index.ts index de3a044..0a4e113 100755 --- a/src/components/Table/Columns/index.ts +++ b/src/components/Table/Columns/index.ts @@ -12,7 +12,6 @@ export { makeNumericColumnPlanFact, makeNumericStartEnd, makeNumericMinMax, - makeNumericAvgRange } from './numeric' export { makeColumnsPlanFact } from './plan_fact' export { makeSelectColumn } from './select' diff --git a/src/components/Table/Columns/numeric.tsx b/src/components/Table/Columns/numeric.tsx index 1b2ed3b..3737ca6 100755 --- a/src/components/Table/Columns/numeric.tsx +++ b/src/components/Table/Columns/numeric.tsx @@ -94,18 +94,4 @@ export const makeNumericMinMax = ( makeNumericColumn('макс', dataIndex + 'Max', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, dataIndex + 'Max')), ]) -export const makeNumericAvgRange = ( - title: ReactNode, - dataIndex: string, - fixed: number, - filters: object[], - filterDelegate: (key: string | number) => any, - renderDelegate: (_: any, row: object) => any, - width: string -) => makeGroupColumn(title, [ - makeNumericColumn('мин', dataIndex + 'Min', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, dataIndex + 'Min')), - makeNumericColumn('сред', dataIndex + 'Avg', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, dataIndex + 'Avg')), - makeNumericColumn('макс', dataIndex + 'Max', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, dataIndex + 'Max')) -]) - export default makeNumericColumn diff --git a/src/components/Table/index.tsx b/src/components/Table/index.tsx index b03b8de..c1b3558 100755 --- a/src/components/Table/index.tsx +++ b/src/components/Table/index.tsx @@ -20,7 +20,6 @@ export { makeNumericColumnPlanFact, makeNumericStartEnd, makeNumericMinMax, - makeNumericAvgRange, makeSelectColumn, makeTagColumn, makeTagInput, diff --git a/src/components/Table/sorters.ts b/src/components/Table/sorters.ts index 11fb7e2..80ca5c0 100755 --- a/src/components/Table/sorters.ts +++ b/src/components/Table/sorters.ts @@ -7,6 +7,9 @@ import { DataType } from './Columns' export const makeNumericSorter = (key: keyof DataType) => (a: DataType, b: DataType) => Number(a[key]) - Number(b[key]) +export const makeNumericObjSorter = (key: [string, string]) => + (a: DataType, b: DataType) => Number(a[key[0]][key[1]]) - Number(b[key[0]][key[1]]) + export const makeStringSorter = (key: keyof DataType) => (a?: DataType | null, b?: DataType | null) => { if (!a && !b) return 0 if (!a) return 1 diff --git a/src/components/d3/D3HorizontalChart.tsx b/src/components/d3/D3HorizontalChart.tsx new file mode 100644 index 0000000..421108b --- /dev/null +++ b/src/components/d3/D3HorizontalChart.tsx @@ -0,0 +1,149 @@ +import React, { memo, useEffect } from 'react' +import * as d3 from 'd3' + +import LoaderPortal from '@components/LoaderPortal' +import { useElementSize } from 'usehooks-ts' + + +import '@styles/d3.less' + +type DataType = { + name: string + percent: number +} + +type D3HorizontalChartProps = { + width?: string + height?: string + data: DataType[] + colors?: string[] +} + +const D3HorizontalChart = memo(( + { + width: givenWidth = '100%', + height: givenHeight = '100%', + data, + colors + }: D3HorizontalChartProps) => { + + const [rootRef, { width, height }] = useElementSize() + + const margin = { top: 50, right: 100, bottom: 50, left: 100 } + + useEffect(() => { + if (width < 100 || height < 100) return + const _width = width - margin.left - margin.right + const _height = height - margin.top - margin.bottom + + const svg = d3.select('#d3-horizontal-chart') + .attr('width', '100%') + .attr('height', '100%') + .append('g') + .attr('transform', `translate(${margin.left},${margin.top})`) + + const percents = ['percents'] + const names = data.map(d => d.name) + + const stackedData = d3.stack() + //@ts-ignore + .keys(percents)(data) + + const xMax = 100 + + // scales + + const x = d3.scaleLinear() + .domain([0, xMax]) + .range([0, _width]) + + const y = d3.scaleBand() + .domain(names) + .range([0, _height]) + .padding(0.25) + + // axes + + const xAxisTop = d3.axisTop(x) + .tickValues([0, 25, 50, 75, 100]) + .tickFormat(d => d + '%') + + const xAxisBottom = d3.axisBottom(x) + .tickValues([0, 25, 50, 75, 100]) + .tickFormat(d => d + '%') + + const yAxisLeft = d3.axisLeft(y) + + const gridlines = d3.axisBottom(x) + .tickValues([0, 25, 50, 75, 100]) + .tickFormat(d => '') + .tickSize(_height) + + const yAxisRight = d3.axisRight(y) + .ticks(0) + .tickValues([]) + .tickFormat(d => '') + + svg.append('g') + .attr('transform', `translate(0,0)`) + .attr("class", "grid-line") + .call(g => g.select('.domain').remove()) + .call(gridlines) + + svg.append('g') + .attr('transform', `translate(0,0)`) + .call(xAxisTop) + + svg.append("g") + .call(yAxisLeft) + + svg.append('g') + .attr('transform', `translate(0,${_height})`) + .call(xAxisBottom) + + svg.append('g') + .attr('transform', `translate(${_width},0)`) + .call(yAxisRight) + + const layers = svg.append('g') + .selectAll('g') + .data(stackedData) + .join('g') + + // transition for bars + const duration = 1000 + const t = d3.transition() + .duration(duration) + .ease(d3.easeLinear) + + layers.each(function() { + d3.select(this) + .selectAll('rect') + //@ts-ignore + .data(d => d) + .join('rect') + .attr('fill', (d, i) => colors ? colors[i] : 'black') + //@ts-ignore + .attr('y', d => y(d.data.name)) + .attr('height', y.bandwidth()) + //@ts-ignore + .transition(t) + //@ts-ignore + .attr('width', d => x(d.data.percent)) + }) + + return () => { + svg.selectAll("g").selectAll("*").remove() + } + }, [width, height, data]) + + return ( + +
+ +
+
+ ) +}) + +export default D3HorizontalChart \ No newline at end of file diff --git a/src/pages/Telemetry/OperationTime/index.tsx b/src/pages/Telemetry/OperationTime/index.tsx new file mode 100644 index 0000000..d442d29 --- /dev/null +++ b/src/pages/Telemetry/OperationTime/index.tsx @@ -0,0 +1,153 @@ +import React, { ReactNode, useEffect, useState } from 'react' +import { Col, Row, Select } from 'antd' +import { Moment } from 'moment' + +import { DateRangeWrapper, makeColumn, makeNumericRender, Table } from '@components/Table' +import LoaderPortal from '@components/LoaderPortal' +import { arrayOrDefault, wrapPrivateComponent } from '@utils' +import D3HorizontalChart from '@components/d3/D3HorizontalChart' +import { useWell } from '@asb/context' +import { invokeWebApiWrapperAsync } from '@components/factory' +import { SubsystemOperationTimeService } from '@api' + +const { Option } = Select; + +type DataType = { + idSubsystem: number + subsystemName: string + usedTimeHours: number + kUsage: number + sumDepthInterval: number + operationCount: number +} + +const subsystemColors = [ + '#1abc9c', + '#16a085', + '#2ecc71', + '#27ae60', + '#3498db', + '#2980b9', + '#9b59b6', + '#8e44ad', + '#34495e', + '#2c3e50', + '#f1c40f', + '#f39c12', + '#e67e22', + '#d35400', + '#e74c3c', + '#c0392b', + '#ecf0f1', + '#bdc3c7', + '#95a5a6', + '#7f8c8d', +] + +const tableColumns = [ + makeColumn('Цвет', 'color', { width: 50, render: (color) => ( +
+ ) }), + makeColumn('Подсистема', 'subsystemName'), + makeColumn('% использования', 'kUsage', { render: val => (+val * 100).toFixed(2) }), + makeColumn('Проходка, м', 'sumDepthInterval', {render: makeNumericRender(2)}), + makeColumn('Время работы, ч', 'usedTimeHours', {render: makeNumericRender(2)}), + makeColumn('Кол-во запусков', 'operationCount'), +] + +const OperationTime = () => { + const [showLoader, setShowLoader] = useState(false) + const [data, setData] = useState([]) + const [dateRange, setDateRange] = useState([]) + const [childrenData, setChildrenData] = useState([]) + const [well] = useWell() + + const errorNotifyText = `Не удалось загрузить данные` + + useEffect(() => { + + invokeWebApiWrapperAsync( + async () => { + if (!well.id) return + try { + setData(arrayOrDefault(await SubsystemOperationTimeService.getStat( + well.id, + undefined, + dateRange[1] ? dateRange[0]?.toISOString() : undefined, + dateRange[1]?.toISOString(), + ))) + } catch(e) { + setData([]) + throw e + } + }, + setShowLoader, + errorNotifyText, + { actionName: 'Получение данных по скважине', well } + ) + }, [dateRange]) + + useEffect(() => { + setChildrenData(data.map((item) => ( + + ))) + }, [data]) + + const selectChange = (value: string[]) => { + + setData(data.reduce((previousValue: DataType[], currentValue) => { + if (value.includes(currentValue.subsystemName)) { + previousValue.push(currentValue) + } + return previousValue + }, [])) + + } + + return ( + +

Фильтр подсистем

+ + + + + + setDateRange(dateRange)} + value={[dateRange[0], dateRange[1]]} + /> + + +
+ ({name: item.subsystemName, percent: item.kUsage * 100}))} + colors={subsystemColors} + width={'100%'} + height={'50vh'} + /> +
+ ({...d, color: subsystemColors[i]}))} + scroll={{ y: '25vh', x: true }} + pagination={false} + /> + + ) +} + +export default wrapPrivateComponent(OperationTime, { + requirements: [], + title: 'Наработка', + route: 'operation_time', +}) \ No newline at end of file diff --git a/src/pages/Telemetry/index.jsx b/src/pages/Telemetry/index.jsx index 9048ba5..d85f7e8 100755 --- a/src/pages/Telemetry/index.jsx +++ b/src/pages/Telemetry/index.jsx @@ -12,6 +12,7 @@ import Messages from './Messages' import Operations from './Operations' import DashboardNNB from './DashboardNNB' import TelemetryView from './TelemetryView' +import OperationTime from './OperationTime' import '@styles/index.css' @@ -30,6 +31,7 @@ const Telemetry = memo(() => { } /> + @@ -43,6 +45,7 @@ const Telemetry = memo(() => { } /> } /> } /> + } /> diff --git a/src/pages/WellOperations/WellDrillParams.jsx b/src/pages/WellOperations/WellDrillParams.jsx index 6391023..486b422 100755 --- a/src/pages/WellOperations/WellDrillParams.jsx +++ b/src/pages/WellOperations/WellDrillParams.jsx @@ -1,17 +1,87 @@ import { useState, useEffect, useCallback, memo, useMemo } from 'react' +import { InputNumber } from 'antd' import { useWell } from '@asb/context' import { EditableTable, makeSelectColumn, - makeNumericAvgRange, + makeGroupColumn, + makeNumericRender, makeNumericSorter, + RegExpIsFloat, } from '@components/Table' import LoaderPortal from '@components/LoaderPortal' import { invokeWebApiWrapperAsync } from '@components/factory' import { DrillParamsService, WellOperationService } from '@api' import { arrayOrDefault } from '@utils' +import { makeNumericObjSorter } from '@components/Table/sorters' + +const makeNumericObjRender = (fixed, columnKey) => (value, obj) => { + let val = '-' + const isSelected = obj && columnKey && obj[columnKey[0]] ? obj[columnKey[0]][columnKey[1]] : false + + if ((value ?? null) !== null && Number.isFinite(+value)) { + val = (fixed ?? null) !== null + ? (+value).toFixed(fixed) + : (+value).toPrecision(5) + } + + return ( +
+ {val} +
+ ) +} + +const makeNumericColumnOptionsWithColor = (fixed, sorterKey, columnKey) => ({ + editable: true, + initialValue: 0, + width: 100, + sorter: sorterKey ? makeNumericObjSorter(sorterKey) : undefined, + formItemRules: [{ + required: true, + message: 'Введите число', + pattern: RegExpIsFloat, + }], + render: makeNumericObjRender(fixed, columnKey), +}) + +const makeNumericObjColumn = ( + title, + dataIndex, + filters, + filterDelegate, + renderDelegate, + width, + other +) => ({ + title: title, + dataIndex: dataIndex, + key: dataIndex, + filters: filters, + onFilter: filterDelegate ? filterDelegate(dataIndex) : null, + sorter: makeNumericObjSorter(dataIndex), + width: width, + input: , + render: renderDelegate ?? makeNumericRender(), + align: 'right', + ...other +}) + +const makeNumericAvgRange = ( + title, + dataIndex, + fixed, + filters, + filterDelegate, + renderDelegate, + width +) => makeGroupColumn(title, [ + makeNumericObjColumn('мин', [dataIndex, 'min'], filters, filterDelegate, renderDelegate, width, makeNumericColumnOptionsWithColor(fixed, [dataIndex, 'min'], [dataIndex, 'isMin'])), + makeNumericObjColumn('сред', [dataIndex, 'avg'], filters, filterDelegate, renderDelegate, width, makeNumericColumnOptionsWithColor(fixed, [dataIndex, 'avg'])), + makeNumericObjColumn('макс', [dataIndex, 'max'], filters, filterDelegate, renderDelegate, width, makeNumericColumnOptionsWithColor(fixed, [dataIndex, 'max'], [dataIndex, 'isMax'])) +]) export const getColumns = async (idWell) => { let sectionTypes = await WellOperationService.getSectionTypes(idWell) diff --git a/src/styles/d3.less b/src/styles/d3.less index c009c17..2e48ca4 100644 --- a/src/styles/d3.less +++ b/src/styles/d3.less @@ -116,6 +116,11 @@ } } +.grid-line line { + stroke: #ddd; + stroke-dasharray: 4 +} + @media (max-width: 1800px) { .asb-d3-chart .adaptive-tooltip { font-size: 11px; diff --git a/src/styles/index.css b/src/styles/index.css index b095532..bde3eb8 100755 --- a/src/styles/index.css +++ b/src/styles/index.css @@ -141,4 +141,16 @@ code { .download-link { height: 32px; padding: 4px 15px; +} + +.ant-table-cell:has(.color-pale-green) { + background-color: #98fb98; +} + +.ant-table-tbody > tr > td.ant-table-cell-row-hover:has( > div.color-pale-green) { + background: #98fb98; +} + +.color-pale-green { + background-color: #98fb98; } \ No newline at end of file