Skip to content

Commit

Permalink
feat: add ChartTooltipContent & change chart.events.pointermove sig…
Browse files Browse the repository at this point in the history
…nature (#13)
  • Loading branch information
korvin89 authored Nov 14, 2024
1 parent b898f4e commit 5aed61d
Show file tree
Hide file tree
Showing 8 changed files with 108 additions and 46 deletions.
34 changes: 23 additions & 11 deletions src/components/ChartInner/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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]);

Expand All @@ -148,15 +148,26 @@ 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;
}

const closest = getClosestPoints({
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<SVGSVGElement> = (event) => {
Expand All @@ -170,7 +181,8 @@ export const ChartInner = (props: Props) => {

const handleMouseLeave: React.MouseEventHandler<SVGSVGElement> = (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<SVGSVGElement> = (event) => {
Expand Down
30 changes: 30 additions & 0 deletions src/components/Tooltip/ChartTooltipContent.tsx
Original file line number Diff line number Diff line change
@@ -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) ? (
<DefaultContent hovered={hovered} xAxis={xAxis} yAxis={yAxis} />
) : (
customTooltip
);
};
34 changes: 20 additions & 14 deletions src/components/Tooltip/DefaultContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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});
Expand All @@ -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) => {
Expand All @@ -77,7 +83,7 @@ export const DefaultContent = ({hovered, xAxis, yAxis}: Props) => {
case 'bar-x': {
const value = (
<React.Fragment>
{series.name}: {getYRowData(yAxis, data)}
{series.name}: {getYRowData(data, yAxis)}
</React.Fragment>
);
return (
Expand All @@ -99,11 +105,11 @@ export const DefaultContent = ({hovered, xAxis, yAxis}: Props) => {
{!isTotal && (
<React.Fragment>
<div key={id} className={b('content-row')}>
<b>{getXRowData(xAxis, data)}</b>
<b>{getXRowData(data, xAxis)}</b>
</div>
<div className={b('content-row')}>
<span>{series.name}&nbsp;</span>
<span>{getYRowData(yAxis, data)}</span>
<span>{getYRowData(data, yAxis)}</span>
</div>
</React.Fragment>
)}
Expand All @@ -116,7 +122,7 @@ export const DefaultContent = ({hovered, xAxis, yAxis}: Props) => {
case 'bar-y': {
const value = (
<React.Fragment>
{series.name}: {getXRowData(xAxis, data)}
{series.name}: {getXRowData(data, xAxis)}
</React.Fragment>
);
return (
Expand Down
25 changes: 10 additions & 15 deletions src/components/Tooltip/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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) ? (
<DefaultContent hovered={hovered} xAxis={xAxis} yAxis={yAxis} />
) : (
customTooltip
);
}, [hovered, tooltip, xAxis, yAxis]);

React.useEffect(() => {
window.dispatchEvent(new CustomEvent('scroll'));
Expand All @@ -55,7 +43,14 @@ export const Tooltip = (props: TooltipProps) => {
placement={['right', 'left', 'top', 'bottom']}
modifiers={[{name: 'preventOverflow', options: {padding: 10, altAxis: true}}]}
>
<div className={b('content')}>{content}</div>
<div className={b('content')}>
<ChartTooltipContent
hovered={hovered}
xAxis={xAxis}
yAxis={yAxis as ChartYAxis}
renderer={tooltip.renderer}
/>
</div>
</Popup>
) : null;
};
2 changes: 2 additions & 0 deletions src/components/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import {validateData} from '../validation';

import {ChartInner} from './ChartInner';

export * from './Tooltip/ChartTooltipContent';

export type ChartRef = {
reflow: () => void;
};
Expand Down
8 changes: 4 additions & 4 deletions src/types/chart/chart.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import type {MeaningfulAny} from '../misc';

import type {ChartTooltipRendererData} from './tooltip';

export type ChartMargin = {
top: number;
right: number;
bottom: number;
left: number;
};

type ChartEventData = {point: MeaningfulAny; series: MeaningfulAny};

export type ChartOptions = {
margin?: Partial<ChartMargin>;
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;
};
};
13 changes: 12 additions & 1 deletion src/types/chart/tooltip.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -76,8 +77,18 @@ export type TooltipDataChunk<T = MeaningfulAny> = (
| TooltipDataChunkWaterfall<T>
) & {closest?: boolean};

export type ChartTooltipRendererData<T = MeaningfulAny> = {
hovered: TooltipDataChunk<T>[];
xAxis?: ChartXAxis;
yAxis?: ChartYAxis;
};

export type ChartTooltip<T = MeaningfulAny> = {
enabled?: boolean;
/** Specifies the renderer for the tooltip. If returned null default tooltip renderer will be used. */
renderer?: (args: {hovered: TooltipDataChunk<T>[]}) => React.ReactElement | null;
renderer?: (args: {
hovered: TooltipDataChunk<T>[];
xAxis?: ChartXAxis;
yAxis?: ChartYAxis;
}) => React.ReactElement | null;
};
8 changes: 7 additions & 1 deletion src/utils/d3-dispatcher.ts
Original file line number Diff line number Diff line change
@@ -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);
};

0 comments on commit 5aed61d

Please sign in to comment.