Добвлен курсор для работы с группами вертикальных графиков

This commit is contained in:
goodmice 2022-07-11 12:52:37 +05:00
parent 6e3fe5f7ee
commit 028c05baac
2 changed files with 213 additions and 0 deletions

View File

@ -0,0 +1,212 @@
import { AreaChartOutlined, BarChartOutlined, BorderOuterOutlined, DotChartOutlined, LineChartOutlined } from '@ant-design/icons'
import { CSSProperties, ReactNode, SVGProps, useEffect, useMemo, useRef, useState } from 'react'
import * as d3 from 'd3'
import { useD3MouseZone } from '@components/d3/D3MouseZone'
import { ChartGroup, ChartSizes } from '@components/d3/D3MonitoringCharts'
import { isDev, usePartialProps } from '@utils'
import { wrapPlugin } from './base'
import { D3TooltipPosition } from './D3Tooltip'
import '@styles/d3.less'
type D3GroupRenderFunction<DataType> = (group: ChartGroup<DataType>, data: DataType[]) => ReactNode
export type D3HorizontalCursorSettings<DataType> = {
width?: number
height?: number
render?: D3GroupRenderFunction<DataType>
position?: D3TooltipPosition
className?: string
style?: CSSProperties
limit?: number
lineStyle?: SVGProps<SVGLineElement>
}
export type D3HorizontalCursorProps<DataType> = D3HorizontalCursorSettings<DataType> & {
groups: ChartGroup<DataType>[]
data: DataType[]
sizes: ChartSizes
yAxis?: d3.ScaleTime<number, number>
}
const defaultLineStyle: SVGProps<SVGLineElement> = {
stroke: 'black',
}
const offsetY = 5
const makeDefaultRender = <DataType,>(): D3GroupRenderFunction<DataType> => (group, data) => (
<>
{data.length > 0 ? group.charts.map((chart) => {
let Icon
switch (chart.type) {
case 'needle': Icon = BarChartOutlined; break
case 'line': Icon = LineChartOutlined; break
case 'point': Icon = DotChartOutlined; break
case 'area': Icon = AreaChartOutlined; break
case 'rect_area': Icon = BorderOuterOutlined; break
}
const xFormat = (d: number | Date) => chart.xAxis.format?.(d) ?? `${(+d).toFixed(2)} ${chart.xAxis.unit ?? ''}`
const yFormat = (d: number) => chart.yAxis.format?.(d) ?? `${d?.toFixed(2)} ${chart.yAxis.unit ?? ''}`
return (
<div className={'tooltip-group'} key={chart.key}>
<div className={'group-label'}>
<Icon style={{ color: chart.color }} />
<span>{chart.label}:</span>
</div>
{data.map((d, i) => (
<span key={`${i}`}>
{xFormat(chart.x(d))} :: {yFormat(chart.y(d))}
</span>
))}
</div>
)
}) : (
<span>Данных нет</span>
)}
</>
)
const _D3HorizontalCursor = <DataType,>({
width = 220,
height = 200,
render = makeDefaultRender<DataType>(),
position: _position = 'bottom',
className = '',
style: _style = {},
limit = 2,
lineStyle: _lineStyle,
data,
groups,
sizes,
yAxis,
}: D3HorizontalCursorProps<DataType>) => {
const zoneRef = useRef(null)
const { mouseState, zoneRect, subscribe } = useD3MouseZone()
const [position, setPosition] = useState<D3TooltipPosition>(_position ?? 'bottom')
const [tooltipBodies, setTooltipBodies] = useState<ReactNode[]>([])
const [tooltipY, setTooltipY] = useState(0)
const [fixed, setFixed] = useState(false)
const lineStyle = usePartialProps(_lineStyle, defaultLineStyle)
const zone = useMemo(() => zoneRef.current ? (() => d3.select(zoneRef.current)) : null, [zoneRef.current])
const getXLine = useMemo(() => zone ? (() => zone().select('.tooltip-x-line')) : null, [zone])
useEffect(() => {
const onMiddleClick = (e: Event) => {
if ((e as MouseEvent).button === 1)
setFixed((prev) => !prev)
}
const unsubscribe = subscribe('auxclick', onMiddleClick)
return () => {
if (unsubscribe)
if (!unsubscribe() && isDev())
console.warn('Не удалось отвязать эвент')
}
}, [subscribe])
useEffect(() => {
if (!zone || !getXLine) return
const z = zone()
if (z.selectAll('line').empty()) {
z.append('line').attr('class', 'tooltip-x-line').style('pointer-events', 'none')
}
getXLine()
.attr('x1', 0)
.attr('x2', zoneRect?.width ?? 0)
}, [zone, getXLine, zoneRect])
useEffect(() => {
if (!getXLine) return
const line = getXLine()
Object.entries(lineStyle).map(([key, value]) => line.attr(key, value))
}, [getXLine, lineStyle])
useEffect(() => {
if (!getXLine || !mouseState || fixed) return
getXLine()
.attr('y1', mouseState.y)
.attr('y2', mouseState.y)
.attr('opacity', mouseState.visible ? 1 : 0)
}, [getXLine, mouseState, fixed])
useEffect(() => {
if (!mouseState.visible || fixed) return
let top = mouseState.y + offsetY
if (top + height >= sizes.chartsHeight) {
setPosition('bottom')
top = mouseState.y - offsetY - height
} else {
setPosition('top')
}
setTooltipY(top)
}, [sizes.chartsHeight, height, mouseState, fixed])
const [lineY, setLineY] = useState<number>(0)
useEffect(() => {
if (fixed || !mouseState.visible) return
setLineY(mouseState.y)
}, [mouseState, fixed])
useEffect(() => {
if (!yAxis || !data || (!fixed && !mouseState.visible)) return
const limitInS = limit * 1000
const currentDate = +yAxis.invert(lineY)
const chartData = data.filter((row: any) => {
const date = +new Date(row.date)
return (date >= currentDate - limitInS) && (date <= currentDate + limitInS)
})
const bodies = groups.map((group) => render(group, chartData))
setTooltipBodies(bodies)
}, [groups, data, yAxis, lineY, fixed, mouseState.visible])
return (
<g
ref={zoneRef}
className={`cursor-zone ${className}`}
>
{groups.map((_, i) => (
<foreignObject
key={`${i}`}
width={width}
height={height}
x={sizes.groupLeft(i) + (sizes.groupWidth - width) / 2}
y={tooltipY}
opacity={fixed || mouseState.visible ? 1 : 0}
pointerEvents={fixed ? 'all' : 'none'}
style={{ transition: 'opacity .1s ease-out', userSelect: fixed ? 'auto' : 'none' }}
>
<div className={`tooltip ${position} ${className}`}>
<div className={'tooltip-content'}>
{tooltipBodies[i]}
</div>
</div>
</foreignObject>
))}
</g>
)
}
export const D3HorizontalCursor = wrapPlugin(_D3HorizontalCursor, true) as typeof _D3HorizontalCursor
export default D3HorizontalCursor

View File

@ -1,5 +1,6 @@
export * from './base'
export * from './D3ContextMenu'
export * from './D3Cursor'
export * from './D3HorizontalCursor'
export * from './D3Legend'
export * from './D3Tooltip'