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 * as d3 from 'd3'
|
||||||
|
|
||||||
import LoaderPortal from '@components/LoaderPortal'
|
import LoaderPortal from '@components/LoaderPortal'
|
||||||
import { formatDate, makePointsOptimizator, usePartialProps } from '@utils'
|
import { formatDate, isDev, makePointsOptimizator, usePartialProps } from '@utils'
|
||||||
|
|
||||||
import D3MouseZone from './D3MouseZone'
|
import D3MouseZone from './D3MouseZone'
|
||||||
import {
|
import {
|
||||||
@ -247,6 +247,12 @@ export const D3Chart = memo<D3ChartProps>(({
|
|||||||
}, [yAxisArea, yAxis, animDurationMs, width, offset, ticks])
|
}, [yAxisArea, yAxis, animDurationMs, width, offset, ticks])
|
||||||
|
|
||||||
useEffect(() => {
|
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) => {
|
setCharts((oldCharts) => {
|
||||||
const charts: ChartRegistry[] = []
|
const charts: ChartRegistry[] = []
|
||||||
|
|
||||||
@ -293,17 +299,18 @@ export const D3Chart = memo<D3ChartProps>(({
|
|||||||
.attr('fill', 'none')
|
.attr('fill', 'none')
|
||||||
|
|
||||||
let d = data
|
let d = data
|
||||||
|
let elms
|
||||||
|
|
||||||
switch (chart.type) {
|
switch (chart.type) {
|
||||||
case 'needle': {
|
case 'needle':
|
||||||
const bars = chart()
|
elms = chart()
|
||||||
.selectAll('line')
|
.selectAll('line')
|
||||||
.data(data)
|
.data(data)
|
||||||
|
|
||||||
bars.exit().remove()
|
elms.exit().remove()
|
||||||
bars.enter().append('line')
|
elms.enter().append('line')
|
||||||
|
|
||||||
const newBars = chart()
|
elms = chart()
|
||||||
.selectAll('line')
|
.selectAll('line')
|
||||||
.transition()
|
.transition()
|
||||||
.duration(chart.animDurationMs ?? animDurationMs)
|
.duration(chart.animDurationMs ?? animDurationMs)
|
||||||
@ -312,9 +319,7 @@ export const D3Chart = memo<D3ChartProps>(({
|
|||||||
.attr('y1', height - offset.bottom - offset.top)
|
.attr('y1', height - offset.bottom - offset.top)
|
||||||
.attr('y2', (d: any) => yAxis(chart.y(d)))
|
.attr('y2', (d: any) => yAxis(chart.y(d)))
|
||||||
|
|
||||||
chart.afterDraw?.(newBars)
|
|
||||||
break
|
break
|
||||||
}
|
|
||||||
case 'line': {
|
case 'line': {
|
||||||
let line = d3.line()
|
let line = d3.line()
|
||||||
.x(d => xAxis(getX(d)))
|
.x(d => xAxis(getX(d)))
|
||||||
@ -339,17 +344,18 @@ export const D3Chart = memo<D3ChartProps>(({
|
|||||||
if (chart().selectAll('path').empty())
|
if (chart().selectAll('path').empty())
|
||||||
chart().append('path')
|
chart().append('path')
|
||||||
|
|
||||||
const lineElm = chart().selectAll('path')
|
elms = chart().selectAll('path')
|
||||||
.transition()
|
.transition()
|
||||||
.duration(chart.animDurationMs ?? animDurationMs)
|
.duration(chart.animDurationMs ?? animDurationMs)
|
||||||
.attr('d', line(d as any))
|
.attr('d', line(d as any))
|
||||||
|
|
||||||
chart.afterDraw?.(lineElm)
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
chart.afterDraw?.(elms)
|
||||||
})
|
})
|
||||||
}, [charts, data, xAxis, yAxis, height])
|
}, [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} />
|
<rect width={width - offset.left - offset.right} height={height - offset.top - offset.bottom} fill={backgroundColor} />
|
||||||
</g>
|
</g>
|
||||||
<D3MouseZone width={width} height={height} offset={offset}>
|
<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?.cursor?.enabled ?? true) && ( <D3Cursor {...plugins?.cursor} /> )}
|
||||||
|
{(plugins?.tooltip?.enabled ?? true) && ( <D3Tooltip {...plugins?.tooltip} charts={charts} /> )}
|
||||||
</D3MouseZone>
|
</D3MouseZone>
|
||||||
</svg>
|
</svg>
|
||||||
</D3ContextMenu>
|
</D3ContextMenu>
|
||||||
|
@ -9,10 +9,13 @@ export type D3MouseState = {
|
|||||||
visible: boolean
|
visible: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SubscribeFunction = (name: keyof SVGElementEventMap, handler: EventListenerOrEventListenerObject) => null | (() => boolean)
|
||||||
|
|
||||||
export type D3MouseZoneContext = {
|
export type D3MouseZoneContext = {
|
||||||
mouseState: D3MouseState,
|
mouseState: D3MouseState,
|
||||||
zone: (() => d3.Selection<any, any, null, undefined>) | null
|
zone: (() => d3.Selection<any, any, null, undefined>) | null
|
||||||
zoneRect: DOMRect | null
|
zoneRect: DOMRect | null
|
||||||
|
subscribe: SubscribeFunction
|
||||||
}
|
}
|
||||||
|
|
||||||
export type D3MouseZoneProps = {
|
export type D3MouseZoneProps = {
|
||||||
@ -29,7 +32,8 @@ export const D3MouseZoneContext = createContext<D3MouseZoneContext>({
|
|||||||
visible: false
|
visible: false
|
||||||
},
|
},
|
||||||
zone: null,
|
zone: null,
|
||||||
zoneRect: null
|
zoneRect: null,
|
||||||
|
subscribe: () => null
|
||||||
})
|
})
|
||||||
|
|
||||||
export const useD3MouseZone = () => useContext(D3MouseZoneContext)
|
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 [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
|
if (!rectRef.current) return null
|
||||||
rectRef.current.addEventListener(name, handler)
|
rectRef.current.addEventListener(name, handler)
|
||||||
return () => {
|
return () => {
|
||||||
|
@ -1,15 +1,22 @@
|
|||||||
import { ReactNode } from 'react'
|
import { CSSProperties, ReactNode, useEffect, useRef, useState } from 'react'
|
||||||
import * as d3 from 'd3'
|
import * as d3 from 'd3'
|
||||||
|
|
||||||
import { useD3MouseZone } from '../D3MouseZone'
|
import { isDev } from '@utils'
|
||||||
|
|
||||||
|
import { D3MouseState, useD3MouseZone } from '../D3MouseZone'
|
||||||
import { wrapPlugin } from './base'
|
import { wrapPlugin } from './base'
|
||||||
|
|
||||||
import '@styles/d3.less'
|
import '@styles/d3.less'
|
||||||
|
|
||||||
|
type D3TooltipPosition = 'bottom' | 'top' | 'left' | 'right' | 'none'
|
||||||
|
|
||||||
export type BaseTooltip<DataType> = {
|
export type BaseTooltip<DataType> = {
|
||||||
render?: (data: DataType) => ReactNode
|
render?: (data: DataType, target: d3.Selection<any, any, null, undefined>, mouseState: D3MouseState) => ReactNode
|
||||||
width?: number | string
|
width?: number | string
|
||||||
height?: number | string
|
height?: number | string
|
||||||
|
style?: CSSProperties
|
||||||
|
position?: D3TooltipPosition
|
||||||
|
className?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AccurateTooltip = { type: 'accurate' }
|
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>> & {
|
export type D3TooltipProps<DataType = Record<string, any>> = Partial<D3TooltipSettings<DataType>> & {
|
||||||
charts: any[],
|
charts: any[],
|
||||||
data: DataType[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const D3Tooltip = wrapPlugin<D3TooltipProps>(({ type = 'accurate', width, height, render, data, charts }) => {
|
const defaultRender = <DataType,>(data: DataType, target: any, mouseState: D3MouseState) => (
|
||||||
const { mouseState, zoneRect } = useD3MouseZone()
|
<>
|
||||||
|
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 (
|
return (
|
||||||
<foreignObject>
|
<foreignObject
|
||||||
{mouseState.visible && render && render(data)}
|
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>
|
</foreignObject>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -3,18 +3,20 @@
|
|||||||
& .tooltip {
|
& .tooltip {
|
||||||
@color: white;
|
@color: white;
|
||||||
@bg-color: rgba(0, 0, 0, .75);
|
@bg-color: rgba(0, 0, 0, .75);
|
||||||
|
@arrow-size: 8px;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
height: 100% - @arrow-size;
|
||||||
|
|
||||||
color: @color;
|
color: @color;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
padding: 0;
|
padding: 5px;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
background-color: @bg-color;
|
background-color: @bg-color;
|
||||||
|
|
||||||
transition: opacity .1s ease-out;
|
transition: opacity .1s ease-out;
|
||||||
|
|
||||||
@arrow-size: 8px;
|
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
content: ' ';
|
content: ' ';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
Loading…
Reference in New Issue
Block a user