Merged in feature/add-page-operation-time (pull request #10)

Доработка странице Наработка

Approved-by: Александр Васильевич Сироткин
This commit is contained in:
Салихов Тимур 2022-10-13 10:34:02 +00:00 committed by Александр Васильевич Сироткин
commit c9885e4603
4 changed files with 41 additions and 29 deletions

View File

@ -11,6 +11,7 @@ const { RangePicker } = DatePicker
export type DateRangeWrapperProps = RangePickerSharedProps<Moment> & { export type DateRangeWrapperProps = RangePickerSharedProps<Moment> & {
value?: RangeValue<Moment>, value?: RangeValue<Moment>,
isUTC?: boolean isUTC?: boolean
allowClear?: boolean
} }
const normalizeDates = (value?: RangeValue<Moment>, isUTC?: boolean): RangeValue<Moment> => { const normalizeDates = (value?: RangeValue<Moment>, isUTC?: boolean): RangeValue<Moment> => {
@ -21,10 +22,10 @@ const normalizeDates = (value?: RangeValue<Moment>, isUTC?: boolean): RangeValue
] ]
} }
export const DateRangeWrapper = memo<DateRangeWrapperProps>(({ value, isUTC, ...other }) => ( export const DateRangeWrapper = memo<DateRangeWrapperProps>(({ value, isUTC, allowClear = false, ...other }) => (
<RangePicker <RangePicker
showTime showTime
allowClear={false} allowClear={allowClear}
format={defaultFormat} format={defaultFormat}
defaultValue={[ defaultValue={[
moment().subtract(1, 'days').startOf('day'), moment().subtract(1, 'days').startOf('day'),

View File

@ -1,4 +1,4 @@
import { memo, useEffect, useMemo, useRef } from 'react' import { memo, useCallback, useEffect, useMemo, useRef } from 'react'
import { useElementSize } from 'usehooks-ts' import { useElementSize } from 'usehooks-ts'
import { Property } from 'csstype' import { Property } from 'csstype'
import * as d3 from 'd3' import * as d3 from 'd3'
@ -20,6 +20,7 @@ export type D3HorizontalChartProps = {
height?: Property.Height height?: Property.Height
data: PercentChartDataType[] data: PercentChartDataType[]
offset?: Partial<ChartOffset> offset?: Partial<ChartOffset>
afterDraw?: (d: d3.Selection<SVGRectElement, PercentChartDataType, SVGGElement, unknown>) => void
} }
const defaultOffset = { top: 50, right: 100, bottom: 50, left: 100 } const defaultOffset = { top: 50, right: 100, bottom: 50, left: 100 }
@ -29,13 +30,14 @@ export const D3HorizontalPercentChart = memo<D3HorizontalChartProps>(({
height: givenHeight = '100%', height: givenHeight = '100%',
offset: givenOffset, offset: givenOffset,
data, data,
afterDraw,
}) => { }) => {
const offset = usePartialProps<ChartOffset>(givenOffset, defaultOffset) const offset = usePartialProps<ChartOffset>(givenOffset, defaultOffset)
const [divRef, { width, height }] = useElementSize() const [divRef, { width, height }] = useElementSize()
const rootRef = useRef<SVGGElement | null>(null) const rootRef = useRef<SVGGElement | null>(null)
const root = useMemo(() => rootRef.current ? d3.select(rootRef.current) : null, [rootRef.current]) const root = useCallback(() => rootRef.current ? d3.select(rootRef.current) : null, [rootRef.current])
const inlineWidth = useMemo(() => width - offset.left - offset.right, [width]) const inlineWidth = useMemo(() => width - offset.left - offset.right, [width])
const inlineHeight = useMemo(() => height - offset.top - offset.bottom, [height]) const inlineHeight = useMemo(() => height - offset.top - offset.bottom, [height])
@ -44,42 +46,50 @@ export const D3HorizontalPercentChart = memo<D3HorizontalChartProps>(({
const yScale = useMemo(() => d3.scaleBand().domain(data.map((d) => d.name)).range([0, inlineHeight]).padding(0.25), [data, inlineHeight]) const yScale = useMemo(() => d3.scaleBand().domain(data.map((d) => d.name)).range([0, inlineHeight]).padding(0.25), [data, inlineHeight])
useEffect(() => { /// Отрисовываем оси X сверху и снизу useEffect(() => { /// Отрисовываем оси X сверху и снизу
if (width < 100 || height < 100 || !root) return const r = root()
if (width < 100 || height < 100 || !r) return
const xAxisTop = d3.axisTop(xScale).tickFormat((d) => `${d}%`).ticks(4).tickSize(-inlineHeight) const xAxisTop = d3.axisTop(xScale).tickFormat((d) => `${d}%`).ticks(4).tickSize(-inlineHeight)
const xAxisBottom = d3.axisBottom(xScale).tickFormat((d) => `${d}%`).ticks(4) const xAxisBottom = d3.axisBottom(xScale).tickFormat((d) => `${d}%`).ticks(4)
root.selectChild<SVGGElement>('.axis.x.bottom').call(xAxisBottom) r.selectChild<SVGGElement>('.axis.x.bottom').call(xAxisBottom)
root.selectChild<SVGGElement>('.axis.x.top').call(xAxisTop) r.selectChild<SVGGElement>('.axis.x.top').call(xAxisTop)
.selectAll('.tick') .selectAll('.tick')
.attr('class', 'tick grid-line') .attr('class', 'tick grid-line')
}, [root, width, height, xScale, inlineHeight]) }, [root, width, height, xScale, inlineHeight])
useEffect(() => { /// Отрисовываем ось Y слева useEffect(() => { /// Отрисовываем ось Y слева
if (width < 100 || height < 100 || !root) return const r = root()
root.selectChild<SVGGElement>('.axis.y.left').call(d3.axisLeft(yScale)) if (width < 100 || height < 100 || !r) return
r.selectChild<SVGGElement>('.axis.y.left').call(d3.axisLeft(yScale))
}, [root, width, height, yScale]) }, [root, width, height, yScale])
useEffect(() => { useEffect(() => {
if (width < 100 || height < 100 || !root) return const r = root()
if (width < 100 || height < 100 || !r) return
const delay = d3.transition().duration(500).ease(d3.easeLinear) const delay = d3.transition().duration(500).ease(d3.easeLinear)
const rects = root.selectChild('.data').selectAll('rect').data(data) const rects = r.selectChild('.data').selectAll('rect').data(data)
rects.enter().append('rect') rects.enter().append('rect')
rects.exit().remove() rects.exit().remove()
root.selectChild<SVGGElement>('.data')
.selectAll<SVGRectElement, PercentChartDataType>('rect') const selectedRects = r.selectChild<SVGGElement>('.data')
.attr('fill', (d) => d.color || 'black') .selectAll<SVGRectElement, PercentChartDataType>('rect')
.attr('y', (d) => yScale(d.name) ?? null)
.attr('height', yScale.bandwidth()) selectedRects.attr('fill', (d) => d.color || 'black')
.transition(delay) .attr('y', (d) => yScale(d.name) ?? null)
.attr('width', (d) => xScale(d.percent)) .attr('height', yScale.bandwidth())
}, [data, width, height, root, yScale, xScale]) .transition(delay)
.attr('width', (d) => d.percent > 0 ? xScale(d.percent) : 0)
afterDraw?.(selectedRects)
}, [data, width, height, root, yScale, xScale, afterDraw])
return ( return (
<LoaderPortal show={false} style={{ width: givenWidth, height: givenHeight }}> <LoaderPortal show={false} style={{ width: givenWidth, height: givenHeight }}>
<div ref={divRef} style={{ width: '100%', height: '100%' }}> <div ref={divRef} style={{ width: '100%', height: '100%' }}>
<svg id={'d3-horizontal-chart'} width={'100%'} height={'100%'}> <svg width={'100%'} height={'100%'}>
<g ref={rootRef} transform={`translate(${offset.left}, ${offset.top})`}> <g ref={rootRef} transform={`translate(${offset.left}, ${offset.top})`}>
<g className={'axis x top'}></g> <g className={'axis x top'}></g>
<g className={'axis x bottom'} transform={`translate(0, ${inlineHeight})`}></g> <g className={'axis x bottom'} transform={`translate(0, ${inlineHeight})`}></g>

View File

@ -22,14 +22,14 @@ const subsystemColors = [
] ]
const tableColumns = [ const tableColumns = [
makeColumn('Цвет', 'color', { width: 50, render: (backgroundColor) => ( makeColumn('Цвет', 'color', { width: 60, render: (backgroundColor) => (
<div className={'table_color'} style={{ backgroundColor }} /> <div className={'table_color'} style={{ backgroundColor }} />
)}), )}),
makeTextColumn('Подсистема', 'subsystemName'), makeTextColumn('Подсистема', 'subsystemName'),
makeNumericColumn('Использование, %', 'kUsage', undefined, undefined, val => (+val * 100).toFixed(2)), makeNumericColumn('Использование, %', 'kUsage', undefined, undefined, val => (+val * 100).toFixed(2), 200),
makeNumericColumn('Проходка, м', 'sumDepthInterval'), makeNumericColumn('Проходка, м', 'sumDepthInterval', undefined, undefined, undefined, 200),
makeNumericColumn('Время работы, ч', 'usedTimeHours'), makeNumericColumn('Время работы, ч', 'usedTimeHours', undefined, undefined, undefined, 200),
makeNumericColumn('Кол-во запусков', 'operationCount', undefined, undefined, makeNumericRender(0)), makeNumericColumn('Кол-во запусков', 'operationCount', undefined, undefined, makeNumericRender(0), 200),
] ]
// Выбор доступен только до текущей даты // Выбор доступен только до текущей даты
@ -65,15 +65,15 @@ export const OperationTime = memo(() => {
if (!well.id) return if (!well.id) return
// Ограничение задаётся только если выбраны обе даты // Ограничение задаётся только если выбраны обе даты
const startDate = dateRange[1] ? dateRange[0]?.toISOString() : undefined const startDate = dateRange?.[1] ? dateRange[0]?.toISOString() : undefined
const endDate = dateRange[1]?.toISOString() const endDate = dateRange?.[1] ? dateRange[1]?.toISOString() : undefined
const data = await SubsystemOperationTimeService.getStat(well.id, undefined, startDate, endDate) const data = await SubsystemOperationTimeService.getStat(well.id, undefined, startDate, endDate)
// Выбираем цвета для подсистем (если цветов не хватает начинаем сначала) // Выбираем цвета для подсистем (если цветов не хватает начинаем сначала)
const coloredData = arrayOrDefault(data).map((d, i) => ({ ...d, color: subsystemColors[i % subsystemColors.length] })) const coloredData = arrayOrDefault(data).map((d, i) => ({ ...d, color: subsystemColors[i % subsystemColors.length] }))
setData(coloredData) setData(coloredData)
setSelected(data.map((d) => d.idSubsystem)) // По-умолчанию выбираем все подсистемы setSelected(data?.map((d) => d.idSubsystem)) // По-умолчанию выбираем все подсистемы
}, },
setShowLoader, setShowLoader,
`Не удалось загрузить данные`, `Не удалось загрузить данные`,
@ -95,6 +95,7 @@ export const OperationTime = memo(() => {
value={selected} value={selected}
/> />
<DateRangeWrapper <DateRangeWrapper
allowClear
onCalendarChange={setDateRange} onCalendarChange={setDateRange}
disabledDate={disabledDates} disabledDate={disabledDates}
disabledTime={disabledTimes} disabledTime={disabledTimes}

View File

@ -1,3 +1,3 @@
.table_color { .table_color {
padding: 5px 0; padding: 5px 0;
} }