Skip to content

Commit

Permalink
Tooltip reads payload from state (#4540)
Browse files Browse the repository at this point in the history
## Description

There's still fallback to generateCategoricalChart payload because this
doesn't cover all cases - I will follow up on that in next PR.

At least all tests are passing for this change and there was no visual
diff either so that's nice.

## Related Issue

#3717

## Motivation and Context

Remove element cloning

## How Has This Been Tested?

npm test, storybooks


https://www.chromatic.com/build?appId=63da8268a0da9970db6992aa&number=961

## Screenshots (if appropriate):

## Types of changes

- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)

## Checklist:

- [ ] My change requires a change to the documentation.
- [ ] I have updated the documentation accordingly.
- [x] I have added tests to cover my changes.
- [ ] I have added a storybook story or extended an existing story to
show my changes
  • Loading branch information
PavelVanecek committed May 17, 2024
1 parent 2f8c8a0 commit 74fa2a3
Show file tree
Hide file tree
Showing 13 changed files with 423 additions and 129 deletions.
2 changes: 1 addition & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@
}
},
{
"files": ["src/state/*Slice.ts"],
"files": ["src/state/*Slice.ts", "test/state/*.spec.tsx"],
// param-reassign is allowed in slices following Redux recommendation: https://redux-toolkit.js.org/usage/immer-reducers#linting-state-mutations
"rules": { "no-param-reassign": ["error", { "props": false }] }
}
Expand Down
8 changes: 4 additions & 4 deletions src/cartesian/Bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -181,9 +181,9 @@ function BarBackground(props: BarBackgroundProps) {
...restOfAllOtherProps
} = allOtherBarProps;

const onMouseEnterFromContext = useMouseEnterItemDispatch(onMouseEnterFromProps);
const onMouseEnterFromContext = useMouseEnterItemDispatch(onMouseEnterFromProps, dataKey);
const onMouseLeaveFromContext = useMouseLeaveItemDispatch(onMouseLeaveFromProps);
const onClickFromContext = useMouseClickItemDispatch(onItemClickFromProps);
const onClickFromContext = useMouseClickItemDispatch(onItemClickFromProps, dataKey);
if (!backgroundFromProps) {
return null;
}
Expand Down Expand Up @@ -255,9 +255,9 @@ function BarRectangles(props: BarRectanglesProps) {
...restOfAllOtherProps
} = rest;

const onMouseEnterFromContext = useMouseEnterItemDispatch(onMouseEnterFromProps);
const onMouseEnterFromContext = useMouseEnterItemDispatch(onMouseEnterFromProps, dataKey);
const onMouseLeaveFromContext = useMouseLeaveItemDispatch(onMouseLeaveFromProps);
const onClickFromContext = useMouseClickItemDispatch(onItemClickFromProps);
const onClickFromContext = useMouseClickItemDispatch(onItemClickFromProps, dataKey);

if (!data) {
return null;
Expand Down
4 changes: 2 additions & 2 deletions src/cartesian/Scatter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -152,9 +152,9 @@ function ScatterSymbols(props: ScatterSymbolsProps) {
...restOfAllOtherProps
} = allOtherScatterProps;

const onMouseEnterFromContext = useMouseEnterItemDispatch(onMouseEnterFromProps);
const onMouseEnterFromContext = useMouseEnterItemDispatch(onMouseEnterFromProps, allOtherScatterProps.dataKey);
const onMouseLeaveFromContext = useMouseLeaveItemDispatch(onMouseLeaveFromProps);
const onClickFromContext = useMouseClickItemDispatch(onItemClickFromProps);
const onClickFromContext = useMouseClickItemDispatch(onItemClickFromProps, allOtherScatterProps.dataKey);

return (
<>
Expand Down
21 changes: 13 additions & 8 deletions src/component/Tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ import { TooltipContextValue, useTooltipContext } from '../context/tooltipContex
import { useAccessibilityLayer } from '../context/accessibilityContext';
import { useGetBoundingClientRect } from '../util/useGetBoundingClientRect';
import { Cursor, CursorDefinition } from './Cursor';
import { useTooltipEventType } from '../state/selectors';
import { selectTooltipPayload, useTooltipEventType } from '../state/selectors';
import { useCursorPortal, useTooltipPortal } from '../context/tooltipPortalContext';
import { TooltipTrigger } from '../chart/types';
import { useAppSelector } from '../state/hooks';
import { TooltipPayload } from '../state/tooltipSlice';

export type ContentType<TValue extends ValueType, TName extends NameType> =
| ReactElement
Expand Down Expand Up @@ -93,6 +95,8 @@ export type TooltipProps<TValue extends ValueType, TName extends NameType> = Omi
wrapperStyle?: CSSProperties;
};

const emptyPayload: TooltipPayload = [];

function TooltipInternal<TValue extends ValueType, TName extends NameType>(props: TooltipProps<TValue, TName>) {
const {
active: activeFromProps,
Expand All @@ -110,16 +114,16 @@ function TooltipInternal<TValue extends ValueType, TName extends NameType>(props
wrapperStyle,
cursor,
shared,
trigger,
portal: portalFromProps,
} = props;
const viewBox = useViewBox();
const accessibilityLayer = useAccessibilityLayer();
const { active: activeFromContext, payload: payloadFromProps, coordinate, label } = useTooltipContext();
// TODO this will fail tests if we use the selector, uncomment and fix
// const payloadFromContext = useAppSelector(state => selectTooltipPayload(state, shared));
// const payload = payloadFromContext?.length > 0 ? payloadFromContext : payloadFromProps;
const payload = payloadFromProps;
const tooltipEventType = useTooltipEventType(shared);
const { active: activeFromContext, payload: payloadFromProps, coordinate, label } = useTooltipContext();
const payloadFromContext = useAppSelector(state => selectTooltipPayload(state, tooltipEventType, trigger));
// TODO remove the fallback to payloadFromProps
const payload: TooltipPayload = payloadFromContext?.length > 0 ? payloadFromContext : payloadFromProps;
const tooltipPortalFromContext = useTooltipPortal();
/*
* The user can set `active=true` on the Tooltip in which case the Tooltip will stay always active,
Expand All @@ -136,9 +140,9 @@ function TooltipInternal<TValue extends ValueType, TName extends NameType>(props
return null;
}

let finalPayload: Payload<TValue, TName>[] = payload ?? [];
let finalPayload: TooltipPayload = payload ?? emptyPayload;
if (!finalIsActive) {
finalPayload = [];
finalPayload = emptyPayload;
}

if (filterNull && finalPayload.length) {
Expand Down Expand Up @@ -171,6 +175,7 @@ function TooltipInternal<TValue extends ValueType, TName extends NameType>(props
>
{renderContent(content, {
...props,
// @ts-expect-error renderContent method expects the payload to be mutable, TODO make it immutable
payload: finalPayload,
label,
active: finalIsActive,
Expand Down
2 changes: 0 additions & 2 deletions src/context/chartLayoutContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import { TooltipContextProvider, TooltipContextValue } from './tooltipContext';
import { PolarRadiusAxisProps } from '../polar/PolarRadiusAxis';
import { PolarAngleAxisProps } from '../polar/PolarAngleAxis';
import { useAppDispatch, useAppSelector } from '../state/hooks';
import { setActiveTooltipIndex } from '../state/tooltipSlice';
import { setPolarAngleAxisMap, setPolarRadiusAxisMap, setXAxisMap, setYAxisMap } from '../state/axisSlice';
import { RechartsRootState } from '../state/store';
import { setLayout } from '../state/layoutSlice';
Expand Down Expand Up @@ -92,7 +91,6 @@ export const ChartLayoutContextProvider = (props: ChartLayoutContextProviderProp
};

const dispatch = useAppDispatch();
dispatch(setActiveTooltipIndex(tooltipContextValue.index));
dispatch(setXAxisMap(xAxisMap));
dispatch(setYAxisMap(yAxisMap));
dispatch(setPolarAngleAxisMap(angleAxisMap));
Expand Down
18 changes: 14 additions & 4 deletions src/context/tooltipContext.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import React, { createContext, useContext } from 'react';
import { ChartCoordinate, Coordinate } from '../util/types';
import { ChartCoordinate, Coordinate, DataKey } from '../util/types';
import { useAppDispatch } from '../state/hooks';
import { setActiveClickItemIndex, setActiveMouseOverItemIndex } from '../state/tooltipSlice';

export type TooltipContextValue = {
label: string;
Expand Down Expand Up @@ -43,31 +45,39 @@ export const MouseLeaveItemDispatchContext = createContext<ActivateTooltipAction
export const MouseClickItemDispatchContext = createContext<ActivateTooltipAction<TooltipPayloadType> | null>(null);

export const useMouseEnterItemDispatch = <T extends TooltipPayloadType>(
onMouseEnterFromProps: undefined | ActivateTooltipAction<T>,
onMouseEnterFromProps: ActivateTooltipAction<T> | undefined,
dataKey: DataKey<any>,
): ActivateTooltipAction<T> => {
const dispatch = useAppDispatch();
const onMouseEnterFromContext: undefined | ActivateTooltipAction<T> = useContext(MouseEnterItemDispatchContext);
return (data: TooltipTriggerInfo<T>, index: number, event: React.MouseEvent<SVGElement>) => {
onMouseEnterFromProps?.(data, index, event);
onMouseEnterFromContext?.(data, index, event);
dispatch(setActiveMouseOverItemIndex({ activeIndex: index, activeDataKey: dataKey }));
};
};

export const useMouseLeaveItemDispatch = <T extends TooltipPayloadType>(
onMouseLeaveFromProps: undefined | ActivateTooltipAction<T>,
): ActivateTooltipAction<T> => {
const dispatch = useAppDispatch();
const onMouseLeaveFromContext: undefined | ActivateTooltipAction<T> = useContext(MouseLeaveItemDispatchContext);
return (data: TooltipTriggerInfo<T>, index: number, event: React.MouseEvent<SVGElement>) => {
onMouseLeaveFromProps?.(data, index, event);
onMouseLeaveFromContext?.(data, index, event);
dispatch(setActiveMouseOverItemIndex({ activeIndex: -1, activeDataKey: undefined }));
};
};

export const useMouseClickItemDispatch = <T extends TooltipPayloadType>(
onMouseClickFromProps: undefined | ActivateTooltipAction<T>,
): undefined | ActivateTooltipAction<T> => {
onMouseClickFromProps: ActivateTooltipAction<T> | undefined,
dataKey: DataKey<any>,
): ActivateTooltipAction<T> | undefined => {
const dispatch = useAppDispatch();
const onMouseClickFromContext: undefined | ActivateTooltipAction<T> = useContext(MouseClickItemDispatchContext);
return (data: TooltipTriggerInfo<T>, index: number, event: React.MouseEvent<SVGElement>) => {
onMouseClickFromProps?.(data, index, event);
onMouseClickFromContext?.(data, index, event);
dispatch(setActiveClickItemIndex({ activeIndex: index, activeDataKey: dataKey }));
};
};
4 changes: 2 additions & 2 deletions src/numberAxis/Funnel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,9 @@ function FunnelTrapezoids(props: FunnelTrapezoidsProps) {
...restOfAllOtherProps
} = allOtherFunnelProps;

const onMouseEnterFromContext = useMouseEnterItemDispatch(onMouseEnterFromProps);
const onMouseEnterFromContext = useMouseEnterItemDispatch(onMouseEnterFromProps, allOtherFunnelProps.dataKey);
const onMouseLeaveFromContext = useMouseLeaveItemDispatch(onMouseLeaveFromProps);
const onClickFromContext = useMouseClickItemDispatch(onItemClickFromProps);
const onClickFromContext = useMouseClickItemDispatch(onItemClickFromProps, allOtherFunnelProps.dataKey);

return trapezoids.map((entry, i) => {
const isActiveIndex = activeShape && activeIndex === i;
Expand Down
4 changes: 2 additions & 2 deletions src/polar/Pie.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -206,9 +206,9 @@ function PieSectors(props: PieSectorsProps) {
...restOfAllOtherProps
} = allOtherPieProps;

const onMouseEnterFromContext = useMouseEnterItemDispatch(onMouseEnterFromProps);
const onMouseEnterFromContext = useMouseEnterItemDispatch(onMouseEnterFromProps, allOtherPieProps.dataKey);
const onMouseLeaveFromContext = useMouseLeaveItemDispatch(onMouseLeaveFromProps);
const onClickFromContext = useMouseClickItemDispatch(onItemClickFromProps);
const onClickFromContext = useMouseClickItemDispatch(onItemClickFromProps, allOtherPieProps.dataKey);

return sectors.map((entry, i) => {
if (entry?.startAngle === 0 && entry?.endAngle === 0 && sectors.length !== 1) return null;
Expand Down
4 changes: 2 additions & 2 deletions src/polar/RadialBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,9 @@ function RadialBarSectors(props: RadialBarSectorsProps) {
...restOfAllOtherProps
} = allOtherRadialBarProps;

const onMouseEnterFromContext = useMouseEnterItemDispatch(onMouseEnterFromProps);
const onMouseEnterFromContext = useMouseEnterItemDispatch(onMouseEnterFromProps, allOtherRadialBarProps.dataKey);
const onMouseLeaveFromContext = useMouseLeaveItemDispatch(onMouseLeaveFromProps);
const onClickFromContext = useMouseClickItemDispatch(onItemClickFromProps);
const onClickFromContext = useMouseClickItemDispatch(onItemClickFromProps, allOtherRadialBarProps.dataKey);

return (
<>
Expand Down
72 changes: 53 additions & 19 deletions src/state/selectors.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from './hooks';
import { RechartsRootState } from './store';
import { TooltipPayload, TooltipPayloadEntry, TooltipState } from './tooltipSlice';
import { TooltipPayload, TooltipPayloadConfiguration, TooltipPayloadEntry, TooltipState } from './tooltipSlice';
import { getTicksOfAxis, getTooltipEntry, getValueByDataKey } from '../util/ChartUtils';
import { ChartDataState } from './chartDataSlice';
import { selectTooltipAxis } from '../context/useTooltipAxis';
import { BaseAxisProps, DataKey, TickItem } from '../util/types';
import { BaseAxisProps, DataKey, TickItem, TooltipEventType } from '../util/types';
import { findEntryInArray } from '../util/DataUtils';
import { TooltipTrigger } from '../chart/types';

export const useChartName = (): string => {
return useAppSelector((state: RechartsRootState) => state.options.chartName);
};

export function useTooltipEventType(shared: boolean | undefined) {
export function useTooltipEventType(shared: boolean | undefined): TooltipEventType {
const defaultTooltipEventType = useAppSelector((state: RechartsRootState) => state.options.defaultTooltipEventType);
const validateTooltipEventTypes = useAppSelector(
(state: RechartsRootState) => state.options.validateTooltipEventTypes,
Expand All @@ -38,7 +39,23 @@ const selectTooltipTicks = createSelector(selectTooltipAxis, (tooltipAxis: BaseA
getTicksOfAxis(tooltipAxis, false, true),
);

const selectActiveIndex = createSelector(selectTooltipState, (tooltipState: TooltipState) => tooltipState.activeIndex);
export function selectActiveIndex(
state: RechartsRootState,
tooltipEventType: TooltipEventType,
trigger: TooltipTrigger,
): number {
const tooltipState: TooltipState = selectTooltipState(state);
if (tooltipEventType === 'item') {
if (trigger === 'hover') {
return tooltipState.itemInteraction.activeMouseOverIndex;
}
return tooltipState.itemInteraction.activeClickIndex;
}
if (trigger === 'hover') {
return tooltipState.axisInteraction.activeMouseOverAxisIndex;
}
return tooltipState.axisInteraction.activeClickAxisIndex;
}

const selectActiveLabel = createSelector(
selectTooltipTicks,
Expand All @@ -47,32 +64,48 @@ const selectActiveLabel = createSelector(
tooltipTicks?.[activeIndex]?.value,
);

function selectFinalData(
dataDefinedOnItem: ReadonlyArray<unknown>,
dataDefinedOnChart: ReadonlyArray<unknown>,
sharedTooltip: boolean | undefined,
) {
function selectFinalData(dataDefinedOnItem: ReadonlyArray<unknown>, dataDefinedOnChart: ReadonlyArray<unknown>) {
/*
* If a payload has data specified directly from the graphical item, prefer that.
* Otherwise, fill in data from the chart level, using the same index.
*/
if (sharedTooltip === false) {
return dataDefinedOnItem;
}
if (dataDefinedOnItem?.length > 0) {
return dataDefinedOnItem;
}
return dataDefinedOnChart;
}

export function selectTooltipPayloadConfigurations(
state: RechartsRootState,
tooltipEventType: TooltipEventType,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
trigger: TooltipTrigger,
): ReadonlyArray<TooltipPayloadConfiguration> {
const tooltipState = selectTooltipState(state);
// if tooltip reacts to axis interaction, then we display all items at the same time.
if (tooltipEventType === 'axis') {
return tooltipState.tooltipItemPayloads;
}
let filterByDataKey: DataKey<any> | undefined;
/*
* By now we already know that tooltipEventType is 'item', so we can only search in itemInteractions.
* item means that only the hovered or clicked item will be present in the tooltip.
*/
if (trigger === 'hover') {
filterByDataKey = tooltipState.itemInteraction.activeMouseOverDataKey;
} else {
filterByDataKey = tooltipState.itemInteraction.activeClickDataKey;
}
return tooltipState.tooltipItemPayloads.filter(tpc => tpc.settings?.dataKey === filterByDataKey);
}

export const combineTooltipPayload = (
tooltipState: TooltipState,
tooltipItemPayloads: ReadonlyArray<TooltipPayloadConfiguration>,
activeIndex: number,
chartDataState: ChartDataState,
tooltipAxis: BaseAxisProps | undefined,
activeLabel: string | undefined,
shared: boolean | undefined,
): TooltipPayload | undefined => {
const { activeIndex, tooltipItemPayloads } = tooltipState;
if (activeIndex === -1) {
return undefined;
}
Expand All @@ -81,7 +114,7 @@ export const combineTooltipPayload = (
const init: Array<TooltipPayloadEntry> = [];

return tooltipItemPayloads.reduce((agg, { dataDefinedOnItem, settings }): Array<TooltipPayloadEntry> => {
const finalData = selectFinalData(dataDefinedOnItem, chartData, shared);
const finalData = selectFinalData(dataDefinedOnItem, chartData);

const sliced = getSliced(finalData, dataStartIndex, dataEndIndex);

Expand Down Expand Up @@ -129,12 +162,13 @@ export const combineTooltipPayload = (

export const selectTooltipPayload: (
state: RechartsRootState,
shared: boolean | undefined,
tooltipEventType: TooltipEventType,
trigger: TooltipTrigger,
) => TooltipPayload | undefined = createSelector(
selectTooltipState,
selectTooltipPayloadConfigurations,
selectActiveIndex,
selectChartData,
selectTooltipAxis,
selectActiveLabel,
(_: void, shared: boolean | undefined) => shared,
combineTooltipPayload,
);

0 comments on commit 74fa2a3

Please sign in to comment.