diff --git a/src/pages/WellOperations/Tvd/TLChart.jsx b/src/pages/WellOperations/Tvd/TLChart.jsx
index b2ed74b..7050c87 100644
--- a/src/pages/WellOperations/Tvd/TLChart.jsx
+++ b/src/pages/WellOperations/Tvd/TLChart.jsx
@@ -8,8 +8,11 @@ import { useIdWell } from '@asb/context'
import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory'
import { DetectedOperationService } from '@api'
+import { unique } from '@utils/filters'
import { formatDate } from '@utils'
+import { makeGetColor } from '.'
+
const defaultOffset = { left: 40, right: 20, top: 20, bottom: 20 }
const zeroDate = moment('2000-01-01 00:00:00')
@@ -39,12 +42,13 @@ export const TLChart = memo(({
backgroundColor = '#0000',
barHeight = 15,
offset = defaultOffset,
- color,
}) => {
const [isLoading, setIsLoading] = useState(false)
const [svgRef, setSvgRef] = useState()
const [data, setData] = useState()
+ const getColor = useMemo(() => makeGetColor(data?.map((row) => row.idCategory).filter(unique)), [data])
+
const [rootRef, { width, height }] = useElementSize()
const idWell = useIdWell()
@@ -119,8 +123,8 @@ export const TLChart = memo(({
.attr('y', (d) => yAxis(moment(d.startTime).startOf('day')) - barHeight / 2)
.attr('width', (d) => xAxis(d.endTime) - xAxis(d.startTime))
.attr('height', barHeight)
- .attr('fill', (d) => color ? color(d.idCategory) : '#0008')
- }, [svgRef, xAxis, yAxis, data, color])
+ .attr('fill', (d) => getColor(d.idCategory))
+ }, [svgRef, xAxis, yAxis, data, getColor])
return (
diff --git a/src/pages/WellOperations/Tvd/TLPie.jsx b/src/pages/WellOperations/Tvd/TLPie.jsx
index 1305ca5..828c856 100644
--- a/src/pages/WellOperations/Tvd/TLPie.jsx
+++ b/src/pages/WellOperations/Tvd/TLPie.jsx
@@ -1,4 +1,4 @@
-import { memo, useEffect, useMemo, useState } from 'react'
+import { memo, useCallback, useEffect, useMemo, useState } from 'react'
import { useElementSize } from 'usehooks-ts'
import { Empty } from 'antd'
import * as d3 from 'd3'
@@ -8,6 +8,11 @@ import { makeColumn, makeNumericColumn, makeTextColumn, Table } from '@component
import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory'
import { DetectedOperationService } from '@api'
+import { unique } from '@utils/filters'
+
+import { makeGetColor } from '.'
+
+import '@styles/tvd.less'
const tableColumns = [
makeColumn('Цвет', 'color', { width: 50, render: (d) => (
@@ -19,10 +24,11 @@ const tableColumns = [
makeNumericColumn('Процент, %', 'percent', undefined, undefined, (d) => d ? d.toFixed(2) : '---', 100)
]
-export const TLPie = memo(({ color }) => {
+export const TLPie = memo(() => {
const [isLoading, setIsLoading] = useState(false)
const [svgRef, setSvgRef] = useState()
const [stats, setStats] = useState([])
+ const [selected, setSelected] = useState([])
const [rootRef, { width, height }] = useElementSize()
@@ -30,20 +36,50 @@ export const TLPie = memo(({ color }) => {
const pie = useMemo(() => d3.pie().value((d) => d.minutesTotal), [])
+ const getColor = useMemo(() => makeGetColor(stats?.map((row) => row.idCategory).filter(unique)), [stats])
+
const tableData = useMemo(() => {
if (!stats) return null
const totalTime = stats.reduce((out, stat) => out + stat.minutesTotal, 0)
return stats.map((stat) => ({
...stat,
- color: color(stat.idCategory),
+ color: getColor(stat.idCategory),
percent: stat.minutesTotal / totalTime * 100,
}))
- }, [stats, color])
+ }, [stats, getColor])
const data = useMemo(() => tableData ? pie(tableData) : null, [tableData])
const radius = useMemo(() => Math.min(width, height) / 2, [width, height])
+ const onRow = useCallback((record) => {
+ const out = {
+ onMouseEnter: () => {
+ d3.selectAll('.tl-pie-part')
+ .filter((d) => d.data.idCategory === record.idCategory)
+ .attr('transform', 'scale(1.05)')
+ },
+ onMouseLeave: () => {
+ d3.selectAll('.tl-pie-part')
+ .filter((d) => d.data.idCategory === record.idCategory)
+ .attr('transform', 'scale(1)')
+ }
+ }
+ if (record.idCategory === selected)
+ out.style = { background: '#FAFAFA' }
+ return out
+ }, [selected])
+
+ const onPieOver = useCallback(function (e, d) {
+ setSelected(d.data.idCategory)
+ d3.select(this).attr('transform', 'scale(1.05)')
+ }, [])
+
+ const onPieOut = useCallback(function (e, d) {
+ setSelected(null)
+ d3.select(this).attr('transform', 'scale(1)')
+ }, [])
+
useEffect(() => {
invokeWebApiWrapperAsync(
async () => {
@@ -66,9 +102,13 @@ export const TLPie = memo(({ color }) => {
const newSlices = slices.enter().append('path')
slices.merge(newSlices)
+ .attr('class', 'tl-pie-part')
.attr('d', d3.arc().innerRadius(radius * 0.4).outerRadius(radius * 0.8))
- .attr('fill', (d) => color ? color(d.data.idCategory) : '#0008')
- }, [svgRef, data, color, radius])
+ .attr('fill', (d) => d.data.color)
+ .attr('data-id', (d) => d.idCategory)
+ .on('mouseover', onPieOver)
+ .on('mouseout', onPieOut)
+ }, [svgRef, data, radius, onPieOver, onPieOut])
useEffect(() => {
if (!data) return
@@ -81,7 +121,8 @@ export const TLPie = memo(({ color }) => {
.data(data, (d) => d.data.category)
lines.exit().remove()
- const newLines = lines.enter().append('polyline')
+ const newLines = lines.enter()
+ .append('polyline')
const abovePi = (d) => (d.startAngle + d.endAngle) / 2 < Math.PI
@@ -118,8 +159,8 @@ export const TLPie = memo(({ color }) => {
{data ? (
-
-
+
+
) : (
-
+
)}
diff --git a/src/pages/WellOperations/Tvd/index.jsx b/src/pages/WellOperations/Tvd/index.jsx
index 8b495c9..550de4c 100644
--- a/src/pages/WellOperations/Tvd/index.jsx
+++ b/src/pages/WellOperations/Tvd/index.jsx
@@ -9,7 +9,6 @@ import { D3Chart } from '@components/d3'
import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory'
import { formatDate, fractionalSum, wrapPrivateComponent, getOperations } from '@utils'
-import { DetectedOperationService } from '@api'
import TLPie from './TLPie'
import TLChart from './TLChart'
@@ -21,12 +20,19 @@ import AdditionalTables from './AdditionalTables'
import '@styles/index.css'
import '@styles/tvd.less'
-const colorArray = [
- '#1abc9c', '#16a085', '#2ecc71', '#27ae60', '#3498db',
- '#2980b9', '#9b59b6', '#8e44ad', '#34495e', '#2c3e50',
- '#f1c40f', '#f39c12', '#e67e22', '#d35400', '#e74c3c',
- '#c0392b', '#ecf0f1', '#bdc3c7', '#95a5a6', '#7f8c8d',
-]
+export const makeGetColor = (types) => (type) => {
+ if (!type) return '#0008'
+ const raw = [
+ '#1abc9c', '#16a085', '#2ecc71', '#27ae60', '#3498db',
+ '#2980b9', '#9b59b6', '#8e44ad', '#34495e', '#2c3e50',
+ '#f1c40f', '#f39c12', '#e67e22', '#d35400', '#e74c3c',
+ '#c0392b', '#ecf0f1', '#bdc3c7', '#95a5a6', '#7f8c8d',
+ ]
+
+ if (!types || types.length <= 0) return raw[type]
+ const i = types.indexOf(type)
+ return i < 0 ? raw[type] : raw[i]
+}
const Item = ({ label, children, ...other }) => (
{label}: {children}
)
@@ -149,7 +155,6 @@ const Tvd = memo(({ idWell: wellId, title, ...other }) => {
const [isLoading, setIsLoading] = useState(false)
const [pointsEnabled, setPointsEnabled] = useState(true)
const [selectedTab, setSelectedTab] = useState('Скрыть')
- const [color, setColor] = useState()
const idWellContext = useIdWell()
const idWell = useMemo(() => wellId ?? idWellContext, [wellId, idWellContext])
@@ -189,20 +194,6 @@ const Tvd = memo(({ idWell: wellId, title, ...other }) => {
)
}, [idWell])
- useEffect(() => {
- invokeWebApiWrapperAsync(
- async () => {
- const cats = await DetectedOperationService.getCategories()
- const color = d3.scaleOrdinal()
- .domain(cats.map((cat) => cat.id))
- .range(colorArray)
- setColor(() => color)
- },
- undefined,
- 'Не удалось получить список типов операций'
- )
- }, [])
-
return (
@@ -250,8 +241,8 @@ const Tvd = memo(({ idWell: wellId, title, ...other }) => {
/>
{selectedTab === 'НПВ' &&
}
- {selectedTab === 'ЕСО' &&
}
- {selectedTab === 'Статистика' &&
}
+ {selectedTab === 'ЕСО' &&
}
+ {selectedTab === 'Статистика' &&
}
diff --git a/src/styles/tvd.less b/src/styles/tvd.less
index 08137fc..4f61f63 100755
--- a/src/styles/tvd.less
+++ b/src/styles/tvd.less
@@ -78,3 +78,30 @@
}
}
}
+
+.empty-wrapper {
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.tl-pie {
+ display: flex;
+ flex-direction: column;
+ align-items: stretch;
+ justify-content: space-between;
+ height: 100%;
+
+ & .tl-pie-chart {
+ flex-grow: 1;
+
+ & .lines {
+ pointer-events: none;
+ }
+ }
+}
+
+.tl-pie-part {
+ transition: transform .1s ease-in-out;
+}