diff --git a/src/components/ChartInner/index.tsx b/src/components/ChartInner/index.tsx index 27462e7..25ee8ad 100644 --- a/src/components/ChartInner/index.tsx +++ b/src/components/ChartInner/index.tsx @@ -15,8 +15,8 @@ import {getYAxisWidth} from '../../hooks/useChartDimensions/utils'; import {getPreparedXAxis} from '../../hooks/useChartOptions/x-axis'; import {getPreparedYAxis} from '../../hooks/useChartOptions/y-axis'; import {useSplit} from '../../hooks/useSplit'; -import type {ChartData} from '../../types'; -import {block, getD3Dispatcher} from '../../utils'; +import type {ChartData, ChartTooltipRendererData, ChartYAxis} from '../../types'; +import {EventType, block, getD3Dispatcher} from '../../utils'; import {getClosestPoints} from '../../utils/chart/get-closest-data'; import {AxisX, AxisY} from '../Axis'; import {Legend} from '../Legend'; @@ -114,19 +114,19 @@ export const ChartInner = (props: Props) => { React.useEffect(() => { if (clickHandler) { - dispatcher.on('click-chart', clickHandler); + dispatcher.on(EventType.CLICK_CHART, clickHandler); } if (pointerMoveHandler) { - dispatcher.on('hover-shape.chart', (...args) => { - const [hoverData, _position, event] = args; - pointerMoveHandler(hoverData, event); + dispatcher.on(EventType.POINTERMOVE_CHART, (...args) => { + const [handlerData, event] = args; + pointerMoveHandler(handlerData, event); }); } return () => { - dispatcher.on('click-chart', null); - dispatcher.on('hover-shape.chart', null); + dispatcher.on(EventType.CLICK_CHART, null); + dispatcher.on(EventType.POINTERMOVE_CHART, null); }; }, [dispatcher, clickHandler, pointerMoveHandler]); @@ -148,7 +148,8 @@ export const ChartInner = (props: Props) => { const x = pointerX - boundsOffsetLeft; const y = pointerY - boundsOffsetTop; if (isOutsideBounds(x, y)) { - dispatcher.call('hover-shape', {}, undefined, undefined, event); + dispatcher.call(EventType.HOVER_SHAPE, {}, undefined); + dispatcher.call(EventType.POINTERMOVE_CHART, {}, undefined, event); return; } @@ -156,7 +157,17 @@ export const ChartInner = (props: Props) => { position: [x, y], shapesData, }); - dispatcher.call('hover-shape', event.target, closest, [pointerX, pointerY], event); + dispatcher.call(EventType.HOVER_SHAPE, event.target, closest, [pointerX, pointerY]); + dispatcher.call( + EventType.POINTERMOVE_CHART, + {}, + { + hovered: closest, + xAxis, + yAxis: yAxis[0] as ChartYAxis, + } satisfies ChartTooltipRendererData, + event, + ); }; const handleMouseMove: React.MouseEventHandler = (event) => { @@ -170,7 +181,8 @@ export const ChartInner = (props: Props) => { const handleMouseLeave: React.MouseEventHandler = (event) => { throttledHandleMouseMove?.cancel(); - dispatcher.call('hover-shape', {}, undefined, undefined, event); + dispatcher.call(EventType.HOVER_SHAPE, {}, undefined); + dispatcher.call(EventType.POINTERMOVE_CHART, {}, undefined, event); }; const handleTouchMove: React.TouchEventHandler = (event) => { diff --git a/src/components/Tooltip/ChartTooltipContent.tsx b/src/components/Tooltip/ChartTooltipContent.tsx new file mode 100644 index 0000000..00232ca --- /dev/null +++ b/src/components/Tooltip/ChartTooltipContent.tsx @@ -0,0 +1,30 @@ +import React from 'react'; + +import isNil from 'lodash/isNil'; + +import type {ChartTooltip, ChartXAxis, ChartYAxis, TooltipDataChunk} from '../../types'; + +import {DefaultContent} from './DefaultContent'; + +export type ChartTooltipContentProps = { + hovered?: TooltipDataChunk[]; + xAxis?: ChartXAxis; + yAxis?: ChartYAxis; + renderer?: ChartTooltip['renderer']; +}; + +export const ChartTooltipContent = (props: ChartTooltipContentProps) => { + const {hovered, xAxis, yAxis, renderer} = props; + + if (!hovered) { + return null; + } + + const customTooltip = renderer?.({hovered, xAxis, yAxis}); + + return isNil(customTooltip) ? ( + + ) : ( + customTooltip + ); +}; diff --git a/src/components/Tooltip/DefaultContent.tsx b/src/components/Tooltip/DefaultContent.tsx index 090b07c..d2678f7 100644 --- a/src/components/Tooltip/DefaultContent.tsx +++ b/src/components/Tooltip/DefaultContent.tsx @@ -3,10 +3,12 @@ import React from 'react'; import {dateTime} from '@gravity-ui/date-utils'; import get from 'lodash/get'; -import type {PreparedAxis, PreparedPieSeries, PreparedWaterfallSeries} from '../../hooks'; +import type {PreparedPieSeries, PreparedWaterfallSeries} from '../../hooks'; import {formatNumber} from '../../libs'; import type { ChartSeriesData, + ChartXAxis, + ChartYAxis, TooltipDataChunk, TreemapSeriesData, WaterfallSeriesData, @@ -17,14 +19,18 @@ const b = block('d3-tooltip'); type Props = { hovered: TooltipDataChunk[]; - xAxis: PreparedAxis; - yAxis: PreparedAxis; + xAxis?: ChartXAxis; + yAxis?: ChartYAxis; }; const DEFAULT_DATE_FORMAT = 'DD.MM.YY'; -const getRowData = (fieldName: 'x' | 'y', axis: PreparedAxis, data: ChartSeriesData) => { - switch (axis.type) { +const getRowData = ( + fieldName: 'x' | 'y', + data: ChartSeriesData, + axis?: ChartXAxis | ChartYAxis, +) => { + switch (axis?.type) { case 'category': { const categories = get(axis, 'categories', [] as string[]); return getDataCategoryValue({axisDirection: fieldName, categories, data}); @@ -44,20 +50,20 @@ const getRowData = (fieldName: 'x' | 'y', axis: PreparedAxis, data: ChartSeriesD } }; -const getXRowData = (xAxis: PreparedAxis, data: ChartSeriesData) => getRowData('x', xAxis, data); +const getXRowData = (data: ChartSeriesData, xAxis?: ChartXAxis) => getRowData('x', data, xAxis); -const getYRowData = (yAxis: PreparedAxis, data: ChartSeriesData) => getRowData('y', yAxis, data); +const getYRowData = (data: ChartSeriesData, yAxis?: ChartYAxis) => getRowData('y', data, yAxis); -const getMeasureValue = (data: TooltipDataChunk[], xAxis: PreparedAxis, yAxis: PreparedAxis) => { +const getMeasureValue = (data: TooltipDataChunk[], xAxis?: ChartXAxis, yAxis?: ChartYAxis) => { if (data.every((item) => ['pie', 'treemap', 'waterfall'].includes(item.series.type))) { return null; } if (data.some((item) => item.series.type === 'bar-y')) { - return getYRowData(yAxis, data[0]?.data); + return getYRowData(data[0]?.data, yAxis); } - return getXRowData(xAxis, data[0]?.data); + return getXRowData(data[0]?.data, xAxis); }; export const DefaultContent = ({hovered, xAxis, yAxis}: Props) => { @@ -77,7 +83,7 @@ export const DefaultContent = ({hovered, xAxis, yAxis}: Props) => { case 'bar-x': { const value = ( - {series.name}: {getYRowData(yAxis, data)} + {series.name}: {getYRowData(data, yAxis)} ); return ( @@ -99,11 +105,11 @@ export const DefaultContent = ({hovered, xAxis, yAxis}: Props) => { {!isTotal && (
- {getXRowData(xAxis, data)} + {getXRowData(data, xAxis)}
{series.name}  - {getYRowData(yAxis, data)} + {getYRowData(data, yAxis)}
)} @@ -116,7 +122,7 @@ export const DefaultContent = ({hovered, xAxis, yAxis}: Props) => { case 'bar-y': { const value = ( - {series.name}: {getXRowData(xAxis, data)} + {series.name}: {getXRowData(data, xAxis)} ); return ( diff --git a/src/components/Tooltip/index.tsx b/src/components/Tooltip/index.tsx index 6a4838d..ab5b710 100644 --- a/src/components/Tooltip/index.tsx +++ b/src/components/Tooltip/index.tsx @@ -2,13 +2,13 @@ import React from 'react'; import {Popup, useVirtualElementRef} from '@gravity-ui/uikit'; import type {Dispatch} from 'd3'; -import isNil from 'lodash/isNil'; import type {PreparedAxis, PreparedTooltip} from '../../hooks'; import {useTooltip} from '../../hooks'; +import type {ChartYAxis} from '../../types'; import {block} from '../../utils'; -import {DefaultContent} from './DefaultContent'; +import {ChartTooltipContent} from './ChartTooltipContent'; import './styles.scss'; @@ -29,18 +29,6 @@ export const Tooltip = (props: TooltipProps) => { const left = (pointerPosition?.[0] || 0) + containerRect.left; const top = (pointerPosition?.[1] || 0) + containerRect.top; const anchorRef = useVirtualElementRef({rect: {top, left}}); - const content = React.useMemo(() => { - if (!hovered) { - return null; - } - - const customTooltip = tooltip.renderer?.({hovered}); - return isNil(customTooltip) ? ( - - ) : ( - customTooltip - ); - }, [hovered, tooltip, xAxis, yAxis]); React.useEffect(() => { window.dispatchEvent(new CustomEvent('scroll')); @@ -55,7 +43,14 @@ export const Tooltip = (props: TooltipProps) => { placement={['right', 'left', 'top', 'bottom']} modifiers={[{name: 'preventOverflow', options: {padding: 10, altAxis: true}}]} > -
{content}
+
+ +
) : null; }; diff --git a/src/components/index.tsx b/src/components/index.tsx index 9a7d8a1..20ed7a6 100644 --- a/src/components/index.tsx +++ b/src/components/index.tsx @@ -11,6 +11,8 @@ import {validateData} from '../validation'; import {ChartInner} from './ChartInner'; +export * from './Tooltip/ChartTooltipContent'; + export type ChartRef = { reflow: () => void; }; diff --git a/src/types/chart/chart.ts b/src/types/chart/chart.ts index 920286b..3aa64f2 100644 --- a/src/types/chart/chart.ts +++ b/src/types/chart/chart.ts @@ -1,5 +1,7 @@ import type {MeaningfulAny} from '../misc'; +import type {ChartTooltipRendererData} from './tooltip'; + export type ChartMargin = { top: number; right: number; @@ -7,12 +9,10 @@ export type ChartMargin = { left: number; }; -type ChartEventData = {point: MeaningfulAny; series: MeaningfulAny}; - export type ChartOptions = { margin?: Partial; events?: { - click?: (data: ChartEventData, event: PointerEvent) => void; - pointermove?: (data: ChartEventData | undefined, event: PointerEvent) => void; + click?: (data: {point: MeaningfulAny; series: MeaningfulAny}, event: PointerEvent) => void; + pointermove?: (data: ChartTooltipRendererData | undefined, event: PointerEvent) => void; }; }; diff --git a/src/types/chart/tooltip.ts b/src/types/chart/tooltip.ts index ed67455..89e524d 100644 --- a/src/types/chart/tooltip.ts +++ b/src/types/chart/tooltip.ts @@ -1,6 +1,7 @@ import type {MeaningfulAny} from '../misc'; import type {AreaSeries, AreaSeriesData} from './area'; +import type {ChartXAxis, ChartYAxis} from './axis'; import type {BarXSeries, BarXSeriesData} from './bar-x'; import type {BarYSeries, BarYSeriesData} from './bar-y'; import type {LineSeries, LineSeriesData} from './line'; @@ -76,8 +77,18 @@ export type TooltipDataChunk = ( | TooltipDataChunkWaterfall ) & {closest?: boolean}; +export type ChartTooltipRendererData = { + hovered: TooltipDataChunk[]; + xAxis?: ChartXAxis; + yAxis?: ChartYAxis; +}; + export type ChartTooltip = { enabled?: boolean; /** Specifies the renderer for the tooltip. If returned null default tooltip renderer will be used. */ - renderer?: (args: {hovered: TooltipDataChunk[]}) => React.ReactElement | null; + renderer?: (args: { + hovered: TooltipDataChunk[]; + xAxis?: ChartXAxis; + yAxis?: ChartYAxis; + }) => React.ReactElement | null; }; diff --git a/src/utils/d3-dispatcher.ts b/src/utils/d3-dispatcher.ts index 91ae411..5439f3b 100644 --- a/src/utils/d3-dispatcher.ts +++ b/src/utils/d3-dispatcher.ts @@ -1,5 +1,11 @@ import {dispatch} from 'd3'; +export const EventType = { + CLICK_CHART: 'click-chart', + HOVER_SHAPE: 'hover-shape', + POINTERMOVE_CHART: 'pointermove-chart', +}; + export const getD3Dispatcher = () => { - return dispatch('hover-shape', 'click-chart'); + return dispatch(EventType.CLICK_CHART, EventType.HOVER_SHAPE, EventType.POINTERMOVE_CHART); };