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 './base'
|
||||||
export * from './D3ContextMenu'
|
export * from './D3ContextMenu'
|
||||||
export * from './D3Cursor'
|
export * from './D3Cursor'
|
||||||
|
export * from './D3HorizontalCursor'
|
||||||
export * from './D3Legend'
|
export * from './D3Legend'
|
||||||
export * from './D3Tooltip'
|
export * from './D3Tooltip'
|
||||||
|
Loading…
Reference in New Issue
Block a user