forked from ddrilling/asb_cloud_front
функций обработки касания вынесены в base из D3Tooltip
This commit is contained in:
parent
d957ac6690
commit
69222104a6
@ -1,25 +1,24 @@
|
|||||||
import { CSSProperties, memo, ReactNode, useCallback, useEffect, useRef, useState } from 'react'
|
import { AreaChartOutlined, BarChartOutlined, BorderOuterOutlined, DotChartOutlined, LineChartOutlined } from '@ant-design/icons'
|
||||||
import { AreaChartOutlined, BarChartOutlined, DotChartOutlined, LineChartOutlined } from '@ant-design/icons'
|
import { CSSProperties, ReactNode, useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import * as d3 from 'd3'
|
import * as d3 from 'd3'
|
||||||
|
|
||||||
import { isDev } from '@utils'
|
import { isDev } from '@utils'
|
||||||
|
|
||||||
import { D3MouseState, useD3MouseZone } from '../D3MouseZone'
|
import { D3MouseState, useD3MouseZone } from '../D3MouseZone'
|
||||||
import { ChartRegistry } from '../types'
|
import { ChartRegistry } from '../types'
|
||||||
import { wrapPlugin } from './base'
|
import { getTouchedElements, wrapPlugin } from './base'
|
||||||
|
|
||||||
import '@styles/d3.less'
|
import '@styles/d3.less'
|
||||||
|
|
||||||
type D3TooltipPosition = 'bottom' | 'top' | 'left' | 'right' | 'none'
|
export type D3TooltipPosition = 'bottom' | 'top' | 'left' | 'right' | 'none'
|
||||||
type D3TooltipTouchType = 'x' | 'y' | 'all'
|
|
||||||
|
|
||||||
export type D3RenderData<DataType> = {
|
export type D3RenderData<DataType> = {
|
||||||
chart: ChartRegistry<DataType>
|
chart: ChartRegistry<DataType>
|
||||||
data: DataType[]
|
data: DataType[]
|
||||||
selection: d3.Selection<any, DataType, any, any>
|
selection?: d3.Selection<any, DataType, any, any>
|
||||||
}[]
|
}
|
||||||
|
|
||||||
export type D3RenderFunction<DataType> = (data: D3RenderData<DataType>, mouseState: D3MouseState) => ReactNode
|
export type D3RenderFunction<DataType> = (data: D3RenderData<DataType>[], mouseState: D3MouseState) => ReactNode
|
||||||
|
|
||||||
export type D3TooltipSettings<DataType> = {
|
export type D3TooltipSettings<DataType> = {
|
||||||
render?: D3RenderFunction<DataType>
|
render?: D3RenderFunction<DataType>
|
||||||
@ -28,11 +27,10 @@ export type D3TooltipSettings<DataType> = {
|
|||||||
style?: CSSProperties
|
style?: CSSProperties
|
||||||
position?: D3TooltipPosition
|
position?: D3TooltipPosition
|
||||||
className?: string
|
className?: string
|
||||||
touchType?: D3TooltipTouchType
|
|
||||||
limit?: number
|
limit?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const makeDefaultRender = <DataType,>(): D3RenderFunction<DataType> => (data, mouseState) => (
|
export const makeDefaultRender = <DataType,>(): D3RenderFunction<DataType> => (data, mouseState) => (
|
||||||
<>
|
<>
|
||||||
{data.length > 0 ? data.map(({ chart, data }) => {
|
{data.length > 0 ? data.map(({ chart, data }) => {
|
||||||
let Icon
|
let Icon
|
||||||
@ -41,6 +39,7 @@ const makeDefaultRender = <DataType,>(): D3RenderFunction<DataType> => (data, mo
|
|||||||
case 'line': Icon = LineChartOutlined; break
|
case 'line': Icon = LineChartOutlined; break
|
||||||
case 'point': Icon = DotChartOutlined; break
|
case 'point': Icon = DotChartOutlined; break
|
||||||
case 'area': Icon = AreaChartOutlined; break
|
case 'area': Icon = AreaChartOutlined; break
|
||||||
|
case 'rect_area': Icon = BorderOuterOutlined; break
|
||||||
// case 'dot': Icon = DotChartOutLined; break
|
// case 'dot': Icon = DotChartOutLined; break
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,72 +69,6 @@ export type D3TooltipProps<DataType> = Partial<D3TooltipSettings<DataType>> & {
|
|||||||
charts: ChartRegistry<DataType>[],
|
charts: ChartRegistry<DataType>[],
|
||||||
}
|
}
|
||||||
|
|
||||||
const getDistance = (x1: number, y1: number, x2: number, y2: number) =>
|
|
||||||
Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2))
|
|
||||||
|
|
||||||
const makeIsCircleTouched = (x: number, y: number, limit: number) => function (this: d3.BaseType, d: any, i: number) {
|
|
||||||
const elm = d3.select(this)
|
|
||||||
const cx = +elm.attr('cx')
|
|
||||||
const cy = +elm.attr('cy')
|
|
||||||
const r = +elm.attr('r')
|
|
||||||
|
|
||||||
if (Number.isNaN(cx + cy + r)) return false
|
|
||||||
const distance = getDistance(x, y, cx, cy)
|
|
||||||
return (distance - r) <= (limit || 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
const makeIsLineTouched = (x: number, y: number, limit: number) => function (this: d3.BaseType, d: any, i: number) {
|
|
||||||
const elm = d3.select(this)
|
|
||||||
const dx = +elm.attr('x1')
|
|
||||||
const y1 = +elm.attr('y1')
|
|
||||||
const y2 = +elm.attr('y2')
|
|
||||||
|
|
||||||
if (Number.isNaN(dx + y1 + y2)) return false
|
|
||||||
|
|
||||||
const ymin = Math.min(y1, y2)
|
|
||||||
const ymax = Math.max(y1, y2)
|
|
||||||
|
|
||||||
const pd = getDistance(x, y, dx, ymin) // Расстояние до верхней точки
|
|
||||||
const distance = (ymin <= y && y <= ymax) ? Math.abs(x - dx) : pd
|
|
||||||
|
|
||||||
return distance <= (limit || 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getTouchedElements = <DataType,>(
|
|
||||||
chart: ChartRegistry<DataType>,
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
limit: number = 0,
|
|
||||||
touchType: D3TooltipTouchType = 'all'
|
|
||||||
): d3.Selection<any, DataType, any, any> => {
|
|
||||||
let nodes: d3.Selection<any, any, any, any>
|
|
||||||
switch (chart.type) {
|
|
||||||
case 'area':
|
|
||||||
case 'line':
|
|
||||||
case 'point': {
|
|
||||||
const tag = chart.point?.shape ?? 'circle'
|
|
||||||
nodes = chart().selectAll(tag)
|
|
||||||
if (touchType === 'all') {
|
|
||||||
switch (tag) {
|
|
||||||
case 'circle':
|
|
||||||
nodes = nodes.filter(makeIsCircleTouched(x, y, chart.tooltip?.limit ?? limit))
|
|
||||||
break
|
|
||||||
case 'line':
|
|
||||||
nodes = nodes.filter(makeIsLineTouched(x, y, chart.tooltip?.limit ?? limit))
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'needle':
|
|
||||||
nodes = chart().selectAll('line')
|
|
||||||
if (touchType === 'all')
|
|
||||||
nodes = nodes.filter(makeIsLineTouched(x, y, chart.tooltip?.limit ?? limit))
|
|
||||||
break
|
|
||||||
}
|
|
||||||
return nodes
|
|
||||||
}
|
|
||||||
|
|
||||||
function _D3Tooltip<DataType extends Record<string, unknown>>({
|
function _D3Tooltip<DataType extends Record<string, unknown>>({
|
||||||
width = 200,
|
width = 200,
|
||||||
height = 120,
|
height = 120,
|
||||||
@ -144,7 +77,6 @@ function _D3Tooltip<DataType extends Record<string, unknown>>({
|
|||||||
position: _position = 'bottom',
|
position: _position = 'bottom',
|
||||||
className = '',
|
className = '',
|
||||||
style: _style = {},
|
style: _style = {},
|
||||||
touchType = 'all',
|
|
||||||
limit = 2
|
limit = 2
|
||||||
}: D3TooltipProps<DataType>) {
|
}: D3TooltipProps<DataType>) {
|
||||||
const { mouseState, zoneRect, subscribe } = useD3MouseZone()
|
const { mouseState, zoneRect, subscribe } = useD3MouseZone()
|
||||||
@ -156,12 +88,12 @@ function _D3Tooltip<DataType extends Record<string, unknown>>({
|
|||||||
|
|
||||||
const tooltipRef = useRef<HTMLDivElement>(null)
|
const tooltipRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
const onMiddleClick = useCallback((e: Event) => {
|
|
||||||
if ((e as MouseEvent).button === 1 && visible)
|
|
||||||
setFixed((prev) => !prev)
|
|
||||||
}, [visible])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const onMiddleClick = (e: Event) => {
|
||||||
|
if ((e as MouseEvent).button === 1 && visible)
|
||||||
|
setFixed((prev) => !prev)
|
||||||
|
}
|
||||||
|
|
||||||
const unsubscribe = subscribe('auxclick', onMiddleClick)
|
const unsubscribe = subscribe('auxclick', onMiddleClick)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@ -169,7 +101,7 @@ function _D3Tooltip<DataType extends Record<string, unknown>>({
|
|||||||
if (!unsubscribe() && isDev())
|
if (!unsubscribe() && isDev())
|
||||||
console.warn('Не удалось отвязать эвент')
|
console.warn('Не удалось отвязать эвент')
|
||||||
}
|
}
|
||||||
}, [onMiddleClick])
|
}, [visible])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!tooltipRef.current || !zoneRect || fixed) return
|
if (!tooltipRef.current || !zoneRect || fixed) return
|
||||||
@ -197,9 +129,9 @@ function _D3Tooltip<DataType extends Record<string, unknown>>({
|
|||||||
if (!mouseState.visible)
|
if (!mouseState.visible)
|
||||||
return setVisible(false)
|
return setVisible(false)
|
||||||
|
|
||||||
const data: D3RenderData<DataType> = []
|
const data: D3RenderData<DataType>[] = []
|
||||||
charts.forEach((chart) => {
|
charts.forEach((chart) => {
|
||||||
const touched = getTouchedElements(chart, mouseState.x, mouseState.y, limit, touchType)
|
const touched = getTouchedElements(chart, mouseState.x, mouseState.y, limit)
|
||||||
|
|
||||||
if (touched.empty()) return
|
if (touched.empty()) return
|
||||||
|
|
||||||
@ -213,7 +145,7 @@ function _D3Tooltip<DataType extends Record<string, unknown>>({
|
|||||||
setVisible(data.length > 0)
|
setVisible(data.length > 0)
|
||||||
if (data.length > 0)
|
if (data.length > 0)
|
||||||
setTooltipBody(render(data, mouseState))
|
setTooltipBody(render(data, mouseState))
|
||||||
}, [charts, touchType, mouseState, fixed, limit])
|
}, [charts, mouseState, fixed, limit])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<foreignObject
|
<foreignObject
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
import { FC, memo } from 'react'
|
import { FC } from 'react'
|
||||||
|
import * as d3 from 'd3'
|
||||||
|
|
||||||
|
import { getDistance, TouchType } from '@utils'
|
||||||
|
|
||||||
|
import { ChartRegistry } from '../types'
|
||||||
|
|
||||||
export type BasePluginSettings = {
|
export type BasePluginSettings = {
|
||||||
enabled?: boolean
|
enabled?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type InferArgs<T> = T extends (...t: [...infer Arg]) => any ? Arg : never
|
|
||||||
type InferReturn<T> = T extends (...t: [...infer Arg]) => infer Res ? Res : never
|
|
||||||
|
|
||||||
export const wrapPlugin = <TProps,>(
|
export const wrapPlugin = <TProps,>(
|
||||||
Component: FC<TProps>,
|
Component: FC<TProps>,
|
||||||
defaultEnabled?: boolean
|
defaultEnabled?: boolean
|
||||||
@ -19,3 +21,105 @@ export const wrapPlugin = <TProps,>(
|
|||||||
|
|
||||||
return wrappedComponent
|
return wrappedComponent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const makeIsCircleTouched = (x: number, y: number, limit: number, type: TouchType = 'all') => function (this: d3.BaseType, d: any, i: number) {
|
||||||
|
const elm = d3.select(this)
|
||||||
|
const cx = +elm.attr('cx')
|
||||||
|
const cy = +elm.attr('cy')
|
||||||
|
const r = +elm.attr('r')
|
||||||
|
|
||||||
|
if (Number.isNaN(cx + cy + r)) return false
|
||||||
|
|
||||||
|
const distance = getDistance(x, y, cx, cy, type)
|
||||||
|
|
||||||
|
return (distance - r) <= limit
|
||||||
|
}
|
||||||
|
|
||||||
|
const makeIsLineTouched = (x: number, y: number, limit: number, type: TouchType = 'all') => function (this: d3.BaseType, d: any, i: number) {
|
||||||
|
const elm = d3.select(this)
|
||||||
|
const dx = +elm.attr('x1')
|
||||||
|
const y1 = +elm.attr('y1')
|
||||||
|
const y2 = +elm.attr('y2')
|
||||||
|
|
||||||
|
if (Number.isNaN(dx + y1 + y2)) return false
|
||||||
|
|
||||||
|
const ymin = Math.min(y1, y2)
|
||||||
|
const ymax = Math.max(y1, y2)
|
||||||
|
const pd = getDistance(x, y, dx, ymin) // Расстояние до верхней точки
|
||||||
|
|
||||||
|
let distance
|
||||||
|
switch (type) {
|
||||||
|
case 'all':
|
||||||
|
distance = (ymin <= y && y <= ymax) ? Math.abs(x - dx) : pd
|
||||||
|
break
|
||||||
|
case 'x':
|
||||||
|
distance = Math.abs(x - dx)
|
||||||
|
break
|
||||||
|
case 'y':
|
||||||
|
distance = (ymin <= y && y <= ymax) ? 0 : Math.min(Math.abs(y - ymin), Math.abs(y - ymax))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return distance <= limit
|
||||||
|
}
|
||||||
|
|
||||||
|
const makeIsRectTouched = (x: number, y: number, limit: number, type: TouchType = 'all') => function (this: d3.BaseType, d: any, i: number) {
|
||||||
|
const elm = d3.select(this)
|
||||||
|
const dx = +elm.attr('x')
|
||||||
|
const dy = +elm.attr('y')
|
||||||
|
const width = +elm.attr('width')
|
||||||
|
const height = +elm.attr('height')
|
||||||
|
|
||||||
|
if (Number.isNaN(x + y + width + height)) return false
|
||||||
|
|
||||||
|
const isOnHorizont = (dx - limit <= x) && (x <= dx + limit + width)
|
||||||
|
const isOnVertical = (dy - limit <= y) && (y <= dy + limit + height)
|
||||||
|
|
||||||
|
if (isOnHorizont && isOnVertical)
|
||||||
|
return true
|
||||||
|
|
||||||
|
switch(type) {
|
||||||
|
case 'all': {
|
||||||
|
const dV = Math.min(getDistance(x, y, x, dy), getDistance(x, y, x, dy + height))
|
||||||
|
const dH = Math.min(getDistance(x, y, dx, y), getDistance(x, y, dx + width, y))
|
||||||
|
return (isOnHorizont && dV <= limit) || (isOnVertical && dH <= limit)
|
||||||
|
}
|
||||||
|
case 'x': return isOnHorizont
|
||||||
|
case 'y': return isOnVertical
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getTouchedElements = <DataType,>(
|
||||||
|
chart: ChartRegistry<DataType>,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
limit: number = 0,
|
||||||
|
type: TouchType = 'all'
|
||||||
|
): d3.Selection<any, DataType, any, any> => {
|
||||||
|
let nodes: d3.Selection<any, any, any, any>
|
||||||
|
switch (chart.type) {
|
||||||
|
case 'area':
|
||||||
|
case 'line':
|
||||||
|
case 'point': {
|
||||||
|
const tag = chart.point?.shape ?? 'circle'
|
||||||
|
nodes = chart().selectAll(tag)
|
||||||
|
switch (tag) {
|
||||||
|
case 'circle':
|
||||||
|
nodes = nodes.filter(makeIsCircleTouched(x, y, chart.tooltip?.limit ?? limit, type))
|
||||||
|
break
|
||||||
|
case 'line':
|
||||||
|
nodes = nodes.filter(makeIsLineTouched(x, y, chart.tooltip?.limit ?? limit, type))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'needle':
|
||||||
|
nodes = chart().selectAll('line')
|
||||||
|
nodes = nodes.filter(makeIsLineTouched(x, y, chart.tooltip?.limit ?? limit, type))
|
||||||
|
break
|
||||||
|
case 'rect_area':
|
||||||
|
nodes = chart().selectAll('rect')
|
||||||
|
nodes = nodes.filter(makeIsRectTouched(x, y, chart.tooltip?.limit ?? limit, type))
|
||||||
|
}
|
||||||
|
return nodes
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user