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 ? ( -
-
+
+
@@ -134,10 +175,11 @@ export const TLPie = memo(({ color }) => { dataSource={tableData} scroll={{ y: '20vh', x: true }} pagination={false} + onRow={onRow} />
) : ( -
+
)} 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; +}