функций обработки касания вынесены в base из D3Tooltip

This commit is contained in:
goodmice 2022-07-11 12:48:55 +05:00
parent d957ac6690
commit 69222104a6
2 changed files with 126 additions and 90 deletions

View File

@ -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

View File

@ -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
}