* Улучшено отображение пончиковой диаграммы

* Добавлены кнопки выгрузки операций
This commit is contained in:
goodmice 2022-08-05 18:26:07 +05:00
parent ba843f0fde
commit f8f4d8f90b
5 changed files with 101 additions and 25 deletions

View File

@ -14,15 +14,16 @@ export const NetGraphExport = memo(({ idWell, ...other }) => {
), [idWell]) ), [idWell])
return ( return (
<Button <div className={'tvd-input-group'}>
icon={<ExportOutlined />} <Button
loading={isFileExporting} icon={<ExportOutlined />}
onClick={onExport} loading={isFileExporting}
style={{ marginRight: '5px' }} onClick={onExport}
{...other} {...other}
> >
Сетевой график Сетевой график
</Button> </Button>
</div>
) )
}) })

View File

@ -0,0 +1,38 @@
import { memo, useCallback, useEffect, useState } from 'react'
import { Button, Input } from 'antd'
import { useIdWell } from '@asb/context'
import { download, invokeWebApiWrapperAsync } from '@components/factory'
import { WellService } from '@api'
export const StatExport = memo(() => {
const [isFileExporting, setIsFileExporting] = useState(false)
const [idCluster, setIdCluster] = useState()
const idWell = useIdWell()
useEffect(() => {
invokeWebApiWrapperAsync(
async () => {
const { idCluster } = await WellService.get(idWell)
setIdCluster(idCluster)
},
setIsFileExporting,
'Не удалось загрузить ID куста'
)
}, [idWell])
const onExport = useCallback((well) => invokeWebApiWrapperAsync(
async () => await download(`/api/DetectedOperation/export?${well ? 'idWell' : 'idCluster'}=${well ? idWell : idCluster}`),
setIsFileExporting,
'Не удалось загрузить файл'
), [idWell, idCluster])
return (
<Input.Group compact>
<Button loading={isFileExporting} onClick={() => onExport(false)}>Выгрузка (куст)</Button>
<Button loading={isFileExporting} onClick={() => onExport(true)}>Выгрузка (скважина)</Button>
</Input.Group>
)
})
export default StatExport

View File

@ -4,10 +4,21 @@ import { Empty } from 'antd'
import * as d3 from 'd3' import * as d3 from 'd3'
import { useIdWell } from '@asb/context' import { useIdWell } from '@asb/context'
import { makeColumn, makeNumericColumn, makeTextColumn, Table } from '@components/Table'
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'
const tableColumns = [
makeColumn('Цвет', 'color', { width: 50, render: (d) => (
<div style={{ backgroundColor: d, padding: '5px 0' }} />
) }),
makeTextColumn('Название', 'category', undefined, undefined, undefined, { width: 300 }),
makeNumericColumn('Время, мин', 'minutesTotal', undefined, undefined, undefined, 100),
makeNumericColumn('Кол-во', 'count', undefined, undefined, (d) => d ? d.toString() : '---', 100),
makeNumericColumn('Процент, %', 'percent', undefined, undefined, (d) => d ? d.toFixed(2) : '---', 100)
]
export const TLPie = memo(({ color }) => { export const TLPie = memo(({ color }) => {
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const [svgRef, setSvgRef] = useState() const [svgRef, setSvgRef] = useState()
@ -17,10 +28,21 @@ export const TLPie = memo(({ color }) => {
const idWell = useIdWell() const idWell = useIdWell()
const pie = useMemo(() => d3.pie().value((d) => d.count), []) const pie = useMemo(() => d3.pie().value((d) => d.minutesTotal), [])
const data = useMemo(() => stats ? pie(stats) : null, [stats, pie])
const radius = useMemo(() => Math.min(width, height) / 2 - 100, [width, height]) 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),
percent: stat.minutesTotal / totalTime * 100,
}))
}, [stats, color])
const data = useMemo(() => tableData ? pie(tableData) : null, [tableData])
const radius = useMemo(() => Math.min(width, height) / 2, [width, height])
useEffect(() => { useEffect(() => {
invokeWebApiWrapperAsync( invokeWebApiWrapperAsync(
@ -88,21 +110,32 @@ export const TLPie = memo(({ color }) => {
}) })
.style('text-anchor', (d) => abovePi(d) ? 'start' : 'end') .style('text-anchor', (d) => abovePi(d) ? 'start' : 'end')
.attr('width', radius * 0.4) .attr('width', radius * 0.4)
.text((d) => `${d.data.category} (${d.data.count})`) .text((d) => `${d.data.percent.toFixed(2)}% (${d.data.minutesTotal.toFixed(2)} мин)`)
}, [svgRef, data, radius]) }, [svgRef, data, radius])
return ( return (
<div ref={rootRef} 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 ? (
<svg ref={setSvgRef} style={{ width: '100%', height: '100%' }}> <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'stretch', justifyContent: 'space-between', height: '100%' }}>
<g transform={`translate(${width / 2}, ${height / 2})`}> <div ref={rootRef} style={{ flexGrow: 1 }}>
<g className={'slices'} stroke={'#0005'} /> <svg ref={setSvgRef} width={'100%'} height={'100%'}>
<g className={'labels'} fill={'black'} /> <g transform={`translate(${width / 2}, ${height / 2})`}>
<g className={'lines'} fill={'none'} stroke={'black'} /> <g className={'slices'} stroke={'#0005'} />
</g> <g className={'labels'} fill={'black'} />
</svg> <g className={'lines'} fill={'none'} stroke={'black'} />
</g>
</svg>
</div>
<Table
size={'small'}
columns={tableColumns}
dataSource={tableData}
scroll={{ y: '20vh', x: true }}
pagination={false}
/>
</div>
) : ( ) : (
<div style={{ height: '100%', display: 'flex', justifyContent: 'center', alignItems: 'center' }}> <div style={{ height: '100%', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
<Empty /> <Empty />

View File

@ -1,6 +1,6 @@
import { LineChartOutlined, LinkOutlined } from '@ant-design/icons' import { LineChartOutlined, LinkOutlined } from '@ant-design/icons'
import { memo, useState, useEffect, useMemo } from 'react' import { memo, useState, useEffect, useMemo } from 'react'
import { Switch, Segmented } from 'antd' import { Switch, Segmented, Button } from 'antd'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import * as d3 from 'd3' import * as d3 from 'd3'
@ -14,6 +14,7 @@ import { DetectedOperationService } from '@api'
import TLPie from './TLPie' import TLPie from './TLPie'
import TLChart from './TLChart' import TLChart from './TLChart'
import NptTable from './NptTable' import NptTable from './NptTable'
import StatExport from './StatExport'
import NetGraphExport from './NetGraphExport' import NetGraphExport from './NetGraphExport'
import AdditionalTables from './AdditionalTables' import AdditionalTables from './AdditionalTables'
@ -21,8 +22,10 @@ import '@styles/index.css'
import '@styles/tvd.less' import '@styles/tvd.less'
const colorArray = [ const colorArray = [
'#1abc9c', '#16a085', '#2ecc71', '#27ae60', '#3498db', '#2980b9', '#9b59b6', '#8e44ad', '#34495e', '#2c3e50', '#1abc9c', '#16a085', '#2ecc71', '#27ae60', '#3498db',
'#f1c40f', '#f39c12', '#e67e22', '#d35400', '#e74c3c', '#c0392b', '#ecf0f1', '#bdc3c7', '#95a5a6', '#7f8c8d', '#2980b9', '#9b59b6', '#8e44ad', '#34495e', '#2c3e50',
'#f1c40f', '#f39c12', '#e67e22', '#d35400', '#e74c3c',
'#c0392b', '#ecf0f1', '#bdc3c7', '#95a5a6', '#7f8c8d',
] ]
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>)
@ -222,6 +225,7 @@ const Tvd = memo(({ idWell: wellId, title, ...other }) => {
</Item> </Item>
</div> </div>
<div className={'tvd-inputs'}> <div className={'tvd-inputs'}>
<StatExport />
<NetGraphExport idWell={idWell} /> <NetGraphExport idWell={idWell} />
<Segmented <Segmented
options={['НПВ', 'ЕСО', 'Статистика', 'Скрыть']} options={['НПВ', 'ЕСО', 'Статистика', 'Скрыть']}

View File

@ -6,7 +6,7 @@
.tvd-top { .tvd-top {
display: flex; display: flex;
align-items: baseline; align-items: center;
justify-content: space-between; justify-content: space-between;
margin-top: 20px; margin-top: 20px;