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/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;