функций обработки касания вынесены в 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, DotChartOutlined, LineChartOutlined } from '@ant-design/icons'
import { AreaChartOutlined, BarChartOutlined, BorderOuterOutlined, DotChartOutlined, LineChartOutlined } from '@ant-design/icons'
import { CSSProperties, ReactNode, useCallback, useEffect, useRef, useState } from 'react'
import * as d3 from 'd3'
import { isDev } from '@utils'
import { D3MouseState, useD3MouseZone } from '../D3MouseZone'
import { ChartRegistry } from '../types'
import { wrapPlugin } from './base'
import { getTouchedElements, wrapPlugin } from './base'
import '@styles/d3.less'
type D3TooltipPosition = 'bottom' | 'top' | 'left' | 'right' | 'none'
type D3TooltipTouchType = 'x' | 'y' | 'all'
export type D3TooltipPosition = 'bottom' | 'top' | 'left' | 'right' | 'none'
export type D3RenderData<DataType> = {
chart: ChartRegistry<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> = {
render?: D3RenderFunction<DataType>
@ -28,11 +27,10 @@ export type D3TooltipSettings<DataType> = {
style?: CSSProperties
position?: D3TooltipPosition
className?: string
touchType?: D3TooltipTouchType
limit?: number
}
const makeDefaultRender = <DataType,>(): D3RenderFunction<DataType> => (data, mouseState) => (
export const makeDefaultRender = <DataType,>(): D3RenderFunction<DataType> => (data, mouseState) => (
<>
{data.length > 0 ? data.map(({ chart, data }) => {
let Icon
@ -41,6 +39,7 @@ const makeDefaultRender = <DataType,>(): D3RenderFunction<DataType> => (data, mo
case 'line': Icon = LineChartOutlined; break
case 'point': Icon = DotChartOutlined; break
case 'area': Icon = AreaChartOutlined; break
case 'rect_area': Icon = BorderOuterOutlined; break
// case 'dot': Icon = DotChartOutLined; break
}
@ -70,72 +69,6 @@ export type D3TooltipProps<DataType> = Partial<D3TooltipSettings<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>>({
width = 200,
height = 120,
@ -144,7 +77,6 @@ function _D3Tooltip<DataType extends Record<string, unknown>>({
position: _position = 'bottom',
className = '',
style: _style = {},
touchType = 'all',
limit = 2
}: D3TooltipProps<DataType>) {
const { mouseState, zoneRect, subscribe } = useD3MouseZone()
@ -156,12 +88,12 @@ function _D3Tooltip<DataType extends Record<string, unknown>>({
const tooltipRef = useRef<HTMLDivElement>(null)
const onMiddleClick = useCallback((e: Event) => {
if ((e as MouseEvent).button === 1 && visible)
setFixed((prev) => !prev)
}, [visible])
useEffect(() => {
const onMiddleClick = (e: Event) => {
if ((e as MouseEvent).button === 1 && visible)
setFixed((prev) => !prev)
}
const unsubscribe = subscribe('auxclick', onMiddleClick)
return () => {
@ -169,7 +101,7 @@ function _D3Tooltip<DataType extends Record<string, unknown>>({
if (!unsubscribe() && isDev())
console.warn('Не удалось отвязать эвент')
}
}, [onMiddleClick])
}, [visible])
useEffect(() => {
if (!tooltipRef.current || !zoneRect || fixed) return
@ -197,9 +129,9 @@ function _D3Tooltip<DataType extends Record<string, unknown>>({
if (!mouseState.visible)
return setVisible(false)
const data: D3RenderData<DataType> = []
const data: D3RenderData<DataType>[] = []
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
@ -213,7 +145,7 @@ function _D3Tooltip<DataType extends Record<string, unknown>>({
setVisible(data.length > 0)
if (data.length > 0)
setTooltipBody(render(data, mouseState))
}, [charts, touchType, mouseState, fixed, limit])
}, [charts, mouseState, fixed, limit])
return (
<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 = {
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,>(
Component: FC<TProps>,
defaultEnabled?: boolean
@ -19,3 +21,105 @@ export const wrapPlugin = <TProps,>(
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
}