forked from ddrilling/asb_cloud_front
* Улучшен метод выбора цвета
* Добавлено выделение зоны на графике и в таблице * Дополнены стили
This commit is contained in:
parent
310253133d
commit
5511c06410
@ -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 (
|
||||
<div className={'tvd-right'} ref={rootRef}>
|
||||
|
@ -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 }) => {
|
||||
<div className={'tvd-right'}>
|
||||
<LoaderPortal show={isLoading} style={{ width: '100%', flexGrow: 1 }}>
|
||||
{data ? (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'stretch', justifyContent: 'space-between', height: '100%' }}>
|
||||
<div ref={rootRef} style={{ flexGrow: 1 }}>
|
||||
<div className={'tl-pie'}>
|
||||
<div className={'tl-pie-chart'} ref={rootRef}>
|
||||
<svg ref={setSvgRef} width={'100%'} height={'100%'}>
|
||||
<g transform={`translate(${width / 2}, ${height / 2})`}>
|
||||
<g className={'slices'} stroke={'#0005'} />
|
||||
@ -134,10 +175,11 @@ export const TLPie = memo(({ color }) => {
|
||||
dataSource={tableData}
|
||||
scroll={{ y: '20vh', x: true }}
|
||||
pagination={false}
|
||||
onRow={onRow}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ height: '100%', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
|
||||
<div className={'empty-wrapper'}>
|
||||
<Empty />
|
||||
</div>
|
||||
)}
|
||||
|
@ -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 }) => (<div className={'tvd-input-group'} {...other}><span>{label}: </span>{children}</div>)
|
||||
|
||||
@ -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 (
|
||||
<div className={'container tvd-page'} {...other}>
|
||||
<div className={'tvd-top'}>
|
||||
@ -250,8 +241,8 @@ const Tvd = memo(({ idWell: wellId, title, ...other }) => {
|
||||
/>
|
||||
</div>
|
||||
{selectedTab === 'НПВ' && <NptTable operations={operations?.fact} />}
|
||||
{selectedTab === 'ЕСО' && <TLChart color={color} />}
|
||||
{selectedTab === 'Статистика' && <TLPie color={color} />}
|
||||
{selectedTab === 'ЕСО' && <TLChart />}
|
||||
{selectedTab === 'Статистика' && <TLPie />}
|
||||
</div>
|
||||
</LoaderPortal>
|
||||
</div>
|
||||
|
@ -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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user