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..8764d34
--- /dev/null
+++ b/src/pages/Telemetry/OperationTime/index.tsx
@@ -0,0 +1,210 @@
+import React, { ReactNode, useEffect, useState } from 'react'
+import { Col, Row, Select } from 'antd'
+import { Moment } from 'moment'
+
+import { DateRangeWrapper, makeColumn, 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
+ k2: number
+}
+
+const _data: DataType[] = [
+ {
+ "idSubsystem": 1,
+ "subsystemName": "Keруррурk",
+ "usedTimeHours": 2,
+ "kUsage": 0.45,
+ "k2": 1
+ },
+ {
+ "idSubsystem": 2,
+ "subsystemName": "Lol",
+ "usedTimeHours": 0,
+ "kUsage": 0.24,
+ "k2": 0
+ },
+ {
+ "idSubsystem": 3,
+ "subsystemName": "kek",
+ "usedTimeHours": 0,
+ "kUsage": 0.37,
+ "k2": 0
+ },
+ {
+ "idSubsystem": 4,
+ "subsystemName": "keklol",
+ "usedTimeHours": 0,
+ "kUsage": 0.73,
+ "k2": 0
+ },
+ {
+ "idSubsystem": 5,
+ "subsystemName": "Keруррурk1",
+ "usedTimeHours": 2,
+ "kUsage": 0.45,
+ "k2": 1
+ },
+ {
+ "idSubsystem": 6,
+ "subsystemName": "Lol2",
+ "usedTimeHours": 0,
+ "kUsage": 0.24,
+ "k2": 0
+ },
+ {
+ "idSubsystem": 7,
+ "subsystemName": "kek3",
+ "usedTimeHours": 0,
+ "kUsage": 0.37,
+ "k2": 0
+ },
+ {
+ "idSubsystem": 8,
+ "subsystemName": "keklol4",
+ "usedTimeHours": 0,
+ "kUsage": 0.73,
+ "k2": 0
+ },
+]
+
+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('Время работы, ч', 'usedTimeHours'),
+ makeColumn('Активность, %', 'kUsage', { render: (d) => d * 100 })
+]
+
+const OperationTime = () => {
+ const [showLoader, setShowLoader] = useState(false)
+ const [data1, setData1] = useState([])
+ const [data, setData] = useState(_data)
+ const [dateRange, setDateRange] = useState([])
+ const [childrenData, setChildrenData] = useState([])
+ const [well] = useWell()
+
+ const errorNotifyText = `Не удалось загрузить данные`
+
+ useEffect(() => {
+
+ invokeWebApiWrapperAsync(
+ async () => {
+ if (!well.id) return
+ try {
+ setData1(arrayOrDefault(await SubsystemOperationTimeService.getStat(
+ well.id,
+ undefined,
+ dateRange[1] ? dateRange[0]?.toISOString() : undefined,
+ dateRange[1]?.toISOString(),
+ )))
+ } catch(e) {
+ setData1([])
+ 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;