forked from ddrilling/asb_cloud_front
Добавлено отображение подсказок
This commit is contained in:
parent
c952abb2b8
commit
5109d73013
@ -5,7 +5,7 @@ import { Property } from 'csstype'
|
||||
import * as d3 from 'd3'
|
||||
|
||||
import LoaderPortal from '@components/LoaderPortal'
|
||||
import { formatDate, makePointsOptimizator, usePartialProps } from '@utils'
|
||||
import { formatDate, isDev, makePointsOptimizator, usePartialProps } from '@utils'
|
||||
|
||||
import D3MouseZone from './D3MouseZone'
|
||||
import {
|
||||
@ -247,6 +247,12 @@ export const D3Chart = memo<D3ChartProps>(({
|
||||
}, [yAxisArea, yAxis, animDurationMs, width, offset, ticks])
|
||||
|
||||
useEffect(() => {
|
||||
if (isDev())
|
||||
for (let i = 0; i < datasets.length - 1; i++)
|
||||
for (let j = i + 1; j < datasets.length; j++)
|
||||
if (datasets[i].key === datasets[j].key)
|
||||
console.warn(`Ключ датасета "${datasets[i].key}" неуникален (индексы ${i} и ${j})!`)
|
||||
|
||||
setCharts((oldCharts) => {
|
||||
const charts: ChartRegistry[] = []
|
||||
|
||||
@ -293,17 +299,18 @@ export const D3Chart = memo<D3ChartProps>(({
|
||||
.attr('fill', 'none')
|
||||
|
||||
let d = data
|
||||
let elms
|
||||
|
||||
switch (chart.type) {
|
||||
case 'needle': {
|
||||
const bars = chart()
|
||||
case 'needle':
|
||||
elms = chart()
|
||||
.selectAll('line')
|
||||
.data(data)
|
||||
|
||||
bars.exit().remove()
|
||||
bars.enter().append('line')
|
||||
elms.exit().remove()
|
||||
elms.enter().append('line')
|
||||
|
||||
const newBars = chart()
|
||||
elms = chart()
|
||||
.selectAll('line')
|
||||
.transition()
|
||||
.duration(chart.animDurationMs ?? animDurationMs)
|
||||
@ -312,9 +319,7 @@ export const D3Chart = memo<D3ChartProps>(({
|
||||
.attr('y1', height - offset.bottom - offset.top)
|
||||
.attr('y2', (d: any) => yAxis(chart.y(d)))
|
||||
|
||||
chart.afterDraw?.(newBars)
|
||||
break
|
||||
}
|
||||
case 'line': {
|
||||
let line = d3.line()
|
||||
.x(d => xAxis(getX(d)))
|
||||
@ -339,17 +344,18 @@ export const D3Chart = memo<D3ChartProps>(({
|
||||
if (chart().selectAll('path').empty())
|
||||
chart().append('path')
|
||||
|
||||
const lineElm = chart().selectAll('path')
|
||||
elms = chart().selectAll('path')
|
||||
.transition()
|
||||
.duration(chart.animDurationMs ?? animDurationMs)
|
||||
.attr('d', line(d as any))
|
||||
|
||||
chart.afterDraw?.(lineElm)
|
||||
break
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
chart.afterDraw?.(elms)
|
||||
})
|
||||
}, [charts, data, xAxis, yAxis, height])
|
||||
|
||||
@ -377,8 +383,8 @@ export const D3Chart = memo<D3ChartProps>(({
|
||||
<rect width={width - offset.left - offset.right} height={height - offset.top - offset.bottom} fill={backgroundColor} />
|
||||
</g>
|
||||
<D3MouseZone width={width} height={height} offset={offset}>
|
||||
{(plugins?.tooltip?.enabled ?? true) && ( <D3Tooltip {...plugins?.tooltip} data={data} charts={charts} /> )}
|
||||
{(plugins?.cursor?.enabled ?? true) && ( <D3Cursor {...plugins?.cursor} /> )}
|
||||
{(plugins?.tooltip?.enabled ?? true) && ( <D3Tooltip {...plugins?.tooltip} charts={charts} /> )}
|
||||
</D3MouseZone>
|
||||
</svg>
|
||||
</D3ContextMenu>
|
||||
|
@ -9,10 +9,13 @@ export type D3MouseState = {
|
||||
visible: boolean
|
||||
}
|
||||
|
||||
type SubscribeFunction = (name: keyof SVGElementEventMap, handler: EventListenerOrEventListenerObject) => null | (() => boolean)
|
||||
|
||||
export type D3MouseZoneContext = {
|
||||
mouseState: D3MouseState,
|
||||
zone: (() => d3.Selection<any, any, null, undefined>) | null
|
||||
zoneRect: DOMRect | null
|
||||
subscribe: SubscribeFunction
|
||||
}
|
||||
|
||||
export type D3MouseZoneProps = {
|
||||
@ -29,7 +32,8 @@ export const D3MouseZoneContext = createContext<D3MouseZoneContext>({
|
||||
visible: false
|
||||
},
|
||||
zone: null,
|
||||
zoneRect: null
|
||||
zoneRect: null,
|
||||
subscribe: () => null
|
||||
})
|
||||
|
||||
export const useD3MouseZone = () => useContext(D3MouseZoneContext)
|
||||
@ -40,7 +44,7 @@ export const D3MouseZone = memo<D3MouseZoneProps>(({ width, height, offset, chil
|
||||
|
||||
const [state, setState] = useState<D3MouseState>({ x: 0, y: 0, visible: false })
|
||||
|
||||
const subscribeEvent = useCallback((name: keyof SVGElementEventMap, handler: EventListenerOrEventListenerObject) => {
|
||||
const subscribeEvent: SubscribeFunction = useCallback((name, handler) => {
|
||||
if (!rectRef.current) return null
|
||||
rectRef.current.addEventListener(name, handler)
|
||||
return () => {
|
||||
|
@ -1,15 +1,22 @@
|
||||
import { ReactNode } from 'react'
|
||||
import { CSSProperties, ReactNode, useEffect, useRef, useState } from 'react'
|
||||
import * as d3 from 'd3'
|
||||
|
||||
import { useD3MouseZone } from '../D3MouseZone'
|
||||
import { isDev } from '@utils'
|
||||
|
||||
import { D3MouseState, useD3MouseZone } from '../D3MouseZone'
|
||||
import { wrapPlugin } from './base'
|
||||
|
||||
import '@styles/d3.less'
|
||||
|
||||
type D3TooltipPosition = 'bottom' | 'top' | 'left' | 'right' | 'none'
|
||||
|
||||
export type BaseTooltip<DataType> = {
|
||||
render?: (data: DataType) => ReactNode
|
||||
render?: (data: DataType, target: d3.Selection<any, any, null, undefined>, mouseState: D3MouseState) => ReactNode
|
||||
width?: number | string
|
||||
height?: number | string
|
||||
style?: CSSProperties
|
||||
position?: D3TooltipPosition
|
||||
className?: string
|
||||
}
|
||||
|
||||
export type AccurateTooltip = { type: 'accurate' }
|
||||
@ -22,15 +29,97 @@ export type D3TooltipSettings<DataType> = BaseTooltip<DataType> & (AccurateToolt
|
||||
|
||||
export type D3TooltipProps<DataType = Record<string, any>> = Partial<D3TooltipSettings<DataType>> & {
|
||||
charts: any[],
|
||||
data: DataType[]
|
||||
}
|
||||
|
||||
export const D3Tooltip = wrapPlugin<D3TooltipProps>(({ type = 'accurate', width, height, render, data, charts }) => {
|
||||
const { mouseState, zoneRect } = useD3MouseZone()
|
||||
const defaultRender = <DataType,>(data: DataType, target: any, mouseState: D3MouseState) => (
|
||||
<>
|
||||
X: {mouseState.x} Y: {mouseState.y}
|
||||
<br/>
|
||||
Data: {JSON.stringify(data)}
|
||||
</>
|
||||
)
|
||||
|
||||
export const D3Tooltip = wrapPlugin<D3TooltipProps>(function D3Tooltip({
|
||||
type = 'accurate',
|
||||
width = 200,
|
||||
height = 100,
|
||||
render = defaultRender,
|
||||
charts,
|
||||
position: _position = 'bottom',
|
||||
className,
|
||||
style: _style = {},
|
||||
...other
|
||||
}) {
|
||||
const { mouseState, zoneRect, subscribe } = useD3MouseZone()
|
||||
const [tooltipBody, setTooltipBody] = useState<any>()
|
||||
const [style, setStyle] = useState<CSSProperties>(_style)
|
||||
const [position, setPosition] = useState<D3TooltipPosition>(_position ?? 'bottom')
|
||||
const [fixed, setFixed] = useState(false)
|
||||
|
||||
const tooltipRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = subscribe('auxclick', (e) => {
|
||||
if ((e as MouseEvent).button === 1)
|
||||
setFixed((prev) => !prev)
|
||||
})
|
||||
|
||||
return () => {
|
||||
if (unsubscribe)
|
||||
if (!unsubscribe() && isDev())
|
||||
console.warn('Не удалось отвязать эвент')
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!tooltipRef.current || !zoneRect || fixed) return
|
||||
const rect = tooltipRef.current.getBoundingClientRect()
|
||||
|
||||
if (!mouseState.visible) {
|
||||
setStyle((prev) => ({
|
||||
...prev,
|
||||
left: -rect.width,
|
||||
top: -rect.height,
|
||||
opacity: 0,
|
||||
}))
|
||||
return
|
||||
}
|
||||
|
||||
const offsetX = -rect.width / 2 // По центру
|
||||
const offsetY = 30 // Чуть выше курсора
|
||||
|
||||
const left = Math.max(0, Math.min(zoneRect.width - rect.width, mouseState.x + offsetX))
|
||||
const top = mouseState.y - offsetY - rect.height
|
||||
|
||||
setStyle((prev) => ({
|
||||
...prev,
|
||||
left,
|
||||
top,
|
||||
opacity: 1
|
||||
}))
|
||||
}, [tooltipRef.current, mouseState, zoneRect, fixed])
|
||||
|
||||
useEffect(() => {
|
||||
if (fixed) return
|
||||
setTooltipBody(render({}, d3.select('.nothing'), mouseState))
|
||||
}, [mouseState, charts, fixed])
|
||||
|
||||
return (
|
||||
<foreignObject>
|
||||
{mouseState.visible && render && render(data)}
|
||||
<foreignObject
|
||||
x={style.left}
|
||||
y={style.top}
|
||||
width={width}
|
||||
height={height}
|
||||
opacity={style.opacity}
|
||||
pointerEvents={fixed ? 'all' : 'none'}
|
||||
>
|
||||
<div
|
||||
{...other}
|
||||
ref={tooltipRef}
|
||||
className={`tooltip ${position} ${className}`}
|
||||
>
|
||||
{tooltipBody}
|
||||
</div>
|
||||
</foreignObject>
|
||||
)
|
||||
})
|
||||
|
@ -3,18 +3,20 @@
|
||||
& .tooltip {
|
||||
@color: white;
|
||||
@bg-color: rgba(0, 0, 0, .75);
|
||||
@arrow-size: 8px;
|
||||
|
||||
width: 100%;
|
||||
height: 100% - @arrow-size;
|
||||
|
||||
color: @color;
|
||||
position: absolute;
|
||||
padding: 0;
|
||||
padding: 5px;
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
background-color: @bg-color;
|
||||
|
||||
transition: opacity .1s ease-out;
|
||||
|
||||
@arrow-size: 8px;
|
||||
|
||||
&::after {
|
||||
content: ' ';
|
||||
position: absolute;
|
||||
|
Loading…
Reference in New Issue
Block a user