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 LoaderPortal from '@components/LoaderPortal'
|
||||||
import { invokeWebApiWrapperAsync } from '@components/factory'
|
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||||
import { DetectedOperationService } from '@api'
|
import { DetectedOperationService } from '@api'
|
||||||
|
import { unique } from '@utils/filters'
|
||||||
import { formatDate } from '@utils'
|
import { formatDate } from '@utils'
|
||||||
|
|
||||||
|
import { makeGetColor } from '.'
|
||||||
|
|
||||||
const defaultOffset = { left: 40, right: 20, top: 20, bottom: 20 }
|
const defaultOffset = { left: 40, right: 20, top: 20, bottom: 20 }
|
||||||
const zeroDate = moment('2000-01-01 00:00:00')
|
const zeroDate = moment('2000-01-01 00:00:00')
|
||||||
|
|
||||||
@ -39,12 +42,13 @@ export const TLChart = memo(({
|
|||||||
backgroundColor = '#0000',
|
backgroundColor = '#0000',
|
||||||
barHeight = 15,
|
barHeight = 15,
|
||||||
offset = defaultOffset,
|
offset = defaultOffset,
|
||||||
color,
|
|
||||||
}) => {
|
}) => {
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
const [svgRef, setSvgRef] = useState()
|
const [svgRef, setSvgRef] = useState()
|
||||||
const [data, setData] = useState()
|
const [data, setData] = useState()
|
||||||
|
|
||||||
|
const getColor = useMemo(() => makeGetColor(data?.map((row) => row.idCategory).filter(unique)), [data])
|
||||||
|
|
||||||
const [rootRef, { width, height }] = useElementSize()
|
const [rootRef, { width, height }] = useElementSize()
|
||||||
|
|
||||||
const idWell = useIdWell()
|
const idWell = useIdWell()
|
||||||
@ -119,8 +123,8 @@ export const TLChart = memo(({
|
|||||||
.attr('y', (d) => yAxis(moment(d.startTime).startOf('day')) - barHeight / 2)
|
.attr('y', (d) => yAxis(moment(d.startTime).startOf('day')) - barHeight / 2)
|
||||||
.attr('width', (d) => xAxis(d.endTime) - xAxis(d.startTime))
|
.attr('width', (d) => xAxis(d.endTime) - xAxis(d.startTime))
|
||||||
.attr('height', barHeight)
|
.attr('height', barHeight)
|
||||||
.attr('fill', (d) => color ? color(d.idCategory) : '#0008')
|
.attr('fill', (d) => getColor(d.idCategory))
|
||||||
}, [svgRef, xAxis, yAxis, data, color])
|
}, [svgRef, xAxis, yAxis, data, getColor])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'tvd-right'} ref={rootRef}>
|
<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 { useElementSize } from 'usehooks-ts'
|
||||||
import { Empty } from 'antd'
|
import { Empty } from 'antd'
|
||||||
import * as d3 from 'd3'
|
import * as d3 from 'd3'
|
||||||
@ -8,6 +8,11 @@ import { makeColumn, makeNumericColumn, makeTextColumn, Table } from '@component
|
|||||||
import LoaderPortal from '@components/LoaderPortal'
|
import LoaderPortal from '@components/LoaderPortal'
|
||||||
import { invokeWebApiWrapperAsync } from '@components/factory'
|
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||||
import { DetectedOperationService } from '@api'
|
import { DetectedOperationService } from '@api'
|
||||||
|
import { unique } from '@utils/filters'
|
||||||
|
|
||||||
|
import { makeGetColor } from '.'
|
||||||
|
|
||||||
|
import '@styles/tvd.less'
|
||||||
|
|
||||||
const tableColumns = [
|
const tableColumns = [
|
||||||
makeColumn('Цвет', 'color', { width: 50, render: (d) => (
|
makeColumn('Цвет', 'color', { width: 50, render: (d) => (
|
||||||
@ -19,10 +24,11 @@ const tableColumns = [
|
|||||||
makeNumericColumn('Процент, %', 'percent', undefined, undefined, (d) => d ? d.toFixed(2) : '---', 100)
|
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 [isLoading, setIsLoading] = useState(false)
|
||||||
const [svgRef, setSvgRef] = useState()
|
const [svgRef, setSvgRef] = useState()
|
||||||
const [stats, setStats] = useState([])
|
const [stats, setStats] = useState([])
|
||||||
|
const [selected, setSelected] = useState([])
|
||||||
|
|
||||||
const [rootRef, { width, height }] = useElementSize()
|
const [rootRef, { width, height }] = useElementSize()
|
||||||
|
|
||||||
@ -30,20 +36,50 @@ export const TLPie = memo(({ color }) => {
|
|||||||
|
|
||||||
const pie = useMemo(() => d3.pie().value((d) => d.minutesTotal), [])
|
const pie = useMemo(() => d3.pie().value((d) => d.minutesTotal), [])
|
||||||
|
|
||||||
|
const getColor = useMemo(() => makeGetColor(stats?.map((row) => row.idCategory).filter(unique)), [stats])
|
||||||
|
|
||||||
const tableData = useMemo(() => {
|
const tableData = useMemo(() => {
|
||||||
if (!stats) return null
|
if (!stats) return null
|
||||||
const totalTime = stats.reduce((out, stat) => out + stat.minutesTotal, 0)
|
const totalTime = stats.reduce((out, stat) => out + stat.minutesTotal, 0)
|
||||||
return stats.map((stat) => ({
|
return stats.map((stat) => ({
|
||||||
...stat,
|
...stat,
|
||||||
color: color(stat.idCategory),
|
color: getColor(stat.idCategory),
|
||||||
percent: stat.minutesTotal / totalTime * 100,
|
percent: stat.minutesTotal / totalTime * 100,
|
||||||
}))
|
}))
|
||||||
}, [stats, color])
|
}, [stats, getColor])
|
||||||
|
|
||||||
const data = useMemo(() => tableData ? pie(tableData) : null, [tableData])
|
const data = useMemo(() => tableData ? pie(tableData) : null, [tableData])
|
||||||
|
|
||||||
const radius = useMemo(() => Math.min(width, height) / 2, [width, height])
|
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(() => {
|
useEffect(() => {
|
||||||
invokeWebApiWrapperAsync(
|
invokeWebApiWrapperAsync(
|
||||||
async () => {
|
async () => {
|
||||||
@ -66,9 +102,13 @@ export const TLPie = memo(({ color }) => {
|
|||||||
const newSlices = slices.enter().append('path')
|
const newSlices = slices.enter().append('path')
|
||||||
|
|
||||||
slices.merge(newSlices)
|
slices.merge(newSlices)
|
||||||
|
.attr('class', 'tl-pie-part')
|
||||||
.attr('d', d3.arc().innerRadius(radius * 0.4).outerRadius(radius * 0.8))
|
.attr('d', d3.arc().innerRadius(radius * 0.4).outerRadius(radius * 0.8))
|
||||||
.attr('fill', (d) => color ? color(d.data.idCategory) : '#0008')
|
.attr('fill', (d) => d.data.color)
|
||||||
}, [svgRef, data, color, radius])
|
.attr('data-id', (d) => d.idCategory)
|
||||||
|
.on('mouseover', onPieOver)
|
||||||
|
.on('mouseout', onPieOut)
|
||||||
|
}, [svgRef, data, radius, onPieOver, onPieOut])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!data) return
|
if (!data) return
|
||||||
@ -81,7 +121,8 @@ export const TLPie = memo(({ color }) => {
|
|||||||
.data(data, (d) => d.data.category)
|
.data(data, (d) => d.data.category)
|
||||||
|
|
||||||
lines.exit().remove()
|
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
|
const abovePi = (d) => (d.startAngle + d.endAngle) / 2 < Math.PI
|
||||||
|
|
||||||
@ -118,8 +159,8 @@ export const TLPie = memo(({ color }) => {
|
|||||||
<div className={'tvd-right'}>
|
<div className={'tvd-right'}>
|
||||||
<LoaderPortal show={isLoading} style={{ width: '100%', flexGrow: 1 }}>
|
<LoaderPortal show={isLoading} style={{ width: '100%', flexGrow: 1 }}>
|
||||||
{data ? (
|
{data ? (
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'stretch', justifyContent: 'space-between', height: '100%' }}>
|
<div className={'tl-pie'}>
|
||||||
<div ref={rootRef} style={{ flexGrow: 1 }}>
|
<div className={'tl-pie-chart'} ref={rootRef}>
|
||||||
<svg ref={setSvgRef} width={'100%'} height={'100%'}>
|
<svg ref={setSvgRef} width={'100%'} height={'100%'}>
|
||||||
<g transform={`translate(${width / 2}, ${height / 2})`}>
|
<g transform={`translate(${width / 2}, ${height / 2})`}>
|
||||||
<g className={'slices'} stroke={'#0005'} />
|
<g className={'slices'} stroke={'#0005'} />
|
||||||
@ -134,10 +175,11 @@ export const TLPie = memo(({ color }) => {
|
|||||||
dataSource={tableData}
|
dataSource={tableData}
|
||||||
scroll={{ y: '20vh', x: true }}
|
scroll={{ y: '20vh', x: true }}
|
||||||
pagination={false}
|
pagination={false}
|
||||||
|
onRow={onRow}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div style={{ height: '100%', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
|
<div className={'empty-wrapper'}>
|
||||||
<Empty />
|
<Empty />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -9,7 +9,6 @@ import { D3Chart } from '@components/d3'
|
|||||||
import LoaderPortal from '@components/LoaderPortal'
|
import LoaderPortal from '@components/LoaderPortal'
|
||||||
import { invokeWebApiWrapperAsync } from '@components/factory'
|
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||||
import { formatDate, fractionalSum, wrapPrivateComponent, getOperations } from '@utils'
|
import { formatDate, fractionalSum, wrapPrivateComponent, getOperations } from '@utils'
|
||||||
import { DetectedOperationService } from '@api'
|
|
||||||
|
|
||||||
import TLPie from './TLPie'
|
import TLPie from './TLPie'
|
||||||
import TLChart from './TLChart'
|
import TLChart from './TLChart'
|
||||||
@ -21,12 +20,19 @@ import AdditionalTables from './AdditionalTables'
|
|||||||
import '@styles/index.css'
|
import '@styles/index.css'
|
||||||
import '@styles/tvd.less'
|
import '@styles/tvd.less'
|
||||||
|
|
||||||
const colorArray = [
|
export const makeGetColor = (types) => (type) => {
|
||||||
'#1abc9c', '#16a085', '#2ecc71', '#27ae60', '#3498db',
|
if (!type) return '#0008'
|
||||||
'#2980b9', '#9b59b6', '#8e44ad', '#34495e', '#2c3e50',
|
const raw = [
|
||||||
'#f1c40f', '#f39c12', '#e67e22', '#d35400', '#e74c3c',
|
'#1abc9c', '#16a085', '#2ecc71', '#27ae60', '#3498db',
|
||||||
'#c0392b', '#ecf0f1', '#bdc3c7', '#95a5a6', '#7f8c8d',
|
'#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>)
|
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 [isLoading, setIsLoading] = useState(false)
|
||||||
const [pointsEnabled, setPointsEnabled] = useState(true)
|
const [pointsEnabled, setPointsEnabled] = useState(true)
|
||||||
const [selectedTab, setSelectedTab] = useState('Скрыть')
|
const [selectedTab, setSelectedTab] = useState('Скрыть')
|
||||||
const [color, setColor] = useState()
|
|
||||||
|
|
||||||
const idWellContext = useIdWell()
|
const idWellContext = useIdWell()
|
||||||
const idWell = useMemo(() => wellId ?? idWellContext, [wellId, idWellContext])
|
const idWell = useMemo(() => wellId ?? idWellContext, [wellId, idWellContext])
|
||||||
@ -189,20 +194,6 @@ const Tvd = memo(({ idWell: wellId, title, ...other }) => {
|
|||||||
)
|
)
|
||||||
}, [idWell])
|
}, [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 (
|
return (
|
||||||
<div className={'container tvd-page'} {...other}>
|
<div className={'container tvd-page'} {...other}>
|
||||||
<div className={'tvd-top'}>
|
<div className={'tvd-top'}>
|
||||||
@ -250,8 +241,8 @@ const Tvd = memo(({ idWell: wellId, title, ...other }) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{selectedTab === 'НПВ' && <NptTable operations={operations?.fact} />}
|
{selectedTab === 'НПВ' && <NptTable operations={operations?.fact} />}
|
||||||
{selectedTab === 'ЕСО' && <TLChart color={color} />}
|
{selectedTab === 'ЕСО' && <TLChart />}
|
||||||
{selectedTab === 'Статистика' && <TLPie color={color} />}
|
{selectedTab === 'Статистика' && <TLPie />}
|
||||||
</div>
|
</div>
|
||||||
</LoaderPortal>
|
</LoaderPortal>
|
||||||
</div>
|
</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