* Улучшен метод выбора цвета

* Добавлено выделение зоны на графике и в таблице
* Дополнены стили
This commit is contained in:
goodmice 2022-08-08 12:35:08 +05:00
parent 310253133d
commit 5511c06410
4 changed files with 101 additions and 37 deletions

View File

@ -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}>

View File

@ -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>
)}

View File

@ -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>

View File

@ -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;
}