forked from ddrilling/asb_cloud_front
Добвлен курсор для работы с группами вертикальных графиков
This commit is contained in:
parent
6e3fe5f7ee
commit
028c05baac
212
src/components/d3/plugins/D3HorizontalCursor.tsx
Normal file
212
src/components/d3/plugins/D3HorizontalCursor.tsx
Normal 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
|
@ -1,5 +1,6 @@
|
||||
export * from './base'
|
||||
export * from './D3ContextMenu'
|
||||
export * from './D3Cursor'
|
||||
export * from './D3HorizontalCursor'
|
||||
export * from './D3Legend'
|
||||
export * from './D3Tooltip'
|
||||
|
Loading…
Reference in New Issue
Block a user