From 028c05baacc0cf5d9b3f7567964360848ec5daa1 Mon Sep 17 00:00:00 2001 From: goodmice Date: Mon, 11 Jul 2022 12:52:37 +0500 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B2=D0=BB=D0=B5=D0=BD=20?= =?UTF-8?q?=D0=BA=D1=83=D1=80=D1=81=D0=BE=D1=80=20=D0=B4=D0=BB=D1=8F=20?= =?UTF-8?q?=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D1=8B=20=D1=81=20=D0=B3=D1=80?= =?UTF-8?q?=D1=83=D0=BF=D0=BF=D0=B0=D0=BC=D0=B8=20=D0=B2=D0=B5=D1=80=D1=82?= =?UTF-8?q?=D0=B8=D0=BA=D0=B0=D0=BB=D1=8C=D0=BD=D1=8B=D1=85=20=D0=B3=D1=80?= =?UTF-8?q?=D0=B0=D1=84=D0=B8=D0=BA=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../d3/plugins/D3HorizontalCursor.tsx | 212 ++++++++++++++++++ src/components/d3/plugins/index.ts | 1 + 2 files changed, 213 insertions(+) create mode 100644 src/components/d3/plugins/D3HorizontalCursor.tsx diff --git a/src/components/d3/plugins/D3HorizontalCursor.tsx b/src/components/d3/plugins/D3HorizontalCursor.tsx new file mode 100644 index 0000000..6bf0ec0 --- /dev/null +++ b/src/components/d3/plugins/D3HorizontalCursor.tsx @@ -0,0 +1,212 @@ +import { AreaChartOutlined, BarChartOutlined, BorderOuterOutlined, DotChartOutlined, LineChartOutlined } from '@ant-design/icons' +import { CSSProperties, ReactNode, SVGProps, useEffect, useMemo, useRef, useState } from 'react' +import * as d3 from 'd3' + +import { useD3MouseZone } from '@components/d3/D3MouseZone' +import { ChartGroup, ChartSizes } from '@components/d3/D3MonitoringCharts' +import { isDev, usePartialProps } from '@utils' + +import { wrapPlugin } from './base' +import { D3TooltipPosition } from './D3Tooltip' + +import '@styles/d3.less' + +type D3GroupRenderFunction = (group: ChartGroup, data: DataType[]) => ReactNode + +export type D3HorizontalCursorSettings = { + width?: number + height?: number + render?: D3GroupRenderFunction + position?: D3TooltipPosition + className?: string + style?: CSSProperties + limit?: number + lineStyle?: SVGProps +} + +export type D3HorizontalCursorProps = D3HorizontalCursorSettings & { + groups: ChartGroup[] + data: DataType[] + sizes: ChartSizes + yAxis?: d3.ScaleTime +} + +const defaultLineStyle: SVGProps = { + stroke: 'black', +} + +const offsetY = 5 + +const makeDefaultRender = (): D3GroupRenderFunction => (group, data) => ( + <> + {data.length > 0 ? group.charts.map((chart) => { + let Icon + switch (chart.type) { + case 'needle': Icon = BarChartOutlined; break + case 'line': Icon = LineChartOutlined; break + case 'point': Icon = DotChartOutlined; break + case 'area': Icon = AreaChartOutlined; break + case 'rect_area': Icon = BorderOuterOutlined; break + } + + const xFormat = (d: number | Date) => chart.xAxis.format?.(d) ?? `${(+d).toFixed(2)} ${chart.xAxis.unit ?? ''}` + const yFormat = (d: number) => chart.yAxis.format?.(d) ?? `${d?.toFixed(2)} ${chart.yAxis.unit ?? ''}` + + return ( +
+
+ + {chart.label}: +
+ {data.map((d, i) => ( + + {xFormat(chart.x(d))} :: {yFormat(chart.y(d))} + + ))} +
+ ) + }) : ( + Данных нет + )} + +) + +const _D3HorizontalCursor = ({ + width = 220, + height = 200, + render = makeDefaultRender(), + position: _position = 'bottom', + className = '', + style: _style = {}, + limit = 2, + lineStyle: _lineStyle, + + data, + groups, + sizes, + yAxis, +}: D3HorizontalCursorProps) => { + const zoneRef = useRef(null) + + const { mouseState, zoneRect, subscribe } = useD3MouseZone() + const [position, setPosition] = useState(_position ?? 'bottom') + const [tooltipBodies, setTooltipBodies] = useState([]) + const [tooltipY, setTooltipY] = useState(0) + const [fixed, setFixed] = useState(false) + + const lineStyle = usePartialProps(_lineStyle, defaultLineStyle) + + const zone = useMemo(() => zoneRef.current ? (() => d3.select(zoneRef.current)) : null, [zoneRef.current]) + const getXLine = useMemo(() => zone ? (() => zone().select('.tooltip-x-line')) : null, [zone]) + + useEffect(() => { + const onMiddleClick = (e: Event) => { + if ((e as MouseEvent).button === 1) + setFixed((prev) => !prev) + } + + const unsubscribe = subscribe('auxclick', onMiddleClick) + + return () => { + if (unsubscribe) + if (!unsubscribe() && isDev()) + console.warn('Не удалось отвязать эвент') + } + }, [subscribe]) + + useEffect(() => { + if (!zone || !getXLine) return + const z = zone() + + if (z.selectAll('line').empty()) { + z.append('line').attr('class', 'tooltip-x-line').style('pointer-events', 'none') + } + + getXLine() + .attr('x1', 0) + .attr('x2', zoneRect?.width ?? 0) + }, [zone, getXLine, zoneRect]) + + useEffect(() => { + if (!getXLine) return + + const line = getXLine() + Object.entries(lineStyle).map(([key, value]) => line.attr(key, value)) + }, [getXLine, lineStyle]) + + useEffect(() => { + if (!getXLine || !mouseState || fixed) return + + getXLine() + .attr('y1', mouseState.y) + .attr('y2', mouseState.y) + .attr('opacity', mouseState.visible ? 1 : 0) + }, [getXLine, mouseState, fixed]) + + useEffect(() => { + if (!mouseState.visible || fixed) return + + let top = mouseState.y + offsetY + if (top + height >= sizes.chartsHeight) { + setPosition('bottom') + top = mouseState.y - offsetY - height + } else { + setPosition('top') + } + + setTooltipY(top) + }, [sizes.chartsHeight, height, mouseState, fixed]) + + const [lineY, setLineY] = useState(0) + + useEffect(() => { + if (fixed || !mouseState.visible) return + setLineY(mouseState.y) + }, [mouseState, fixed]) + + useEffect(() => { + if (!yAxis || !data || (!fixed && !mouseState.visible)) return + + const limitInS = limit * 1000 + const currentDate = +yAxis.invert(lineY) + + const chartData = data.filter((row: any) => { + const date = +new Date(row.date) + return (date >= currentDate - limitInS) && (date <= currentDate + limitInS) + }) + + const bodies = groups.map((group) => render(group, chartData)) + + setTooltipBodies(bodies) + }, [groups, data, yAxis, lineY, fixed, mouseState.visible]) + + return ( + + {groups.map((_, i) => ( + +
+
+ {tooltipBodies[i]} +
+
+
+ ))} +
+ ) +} + +export const D3HorizontalCursor = wrapPlugin(_D3HorizontalCursor, true) as typeof _D3HorizontalCursor + +export default D3HorizontalCursor diff --git a/src/components/d3/plugins/index.ts b/src/components/d3/plugins/index.ts index 40c5d84..c2fa895 100644 --- a/src/components/d3/plugins/index.ts +++ b/src/components/d3/plugins/index.ts @@ -1,5 +1,6 @@ export * from './base' export * from './D3ContextMenu' export * from './D3Cursor' +export * from './D3HorizontalCursor' export * from './D3Legend' export * from './D3Tooltip'