Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DO NOT MERGE: Request for comments: Plan 5a Proof of concept #3852

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
157 changes: 56 additions & 101 deletions src/chart/generateCategoricalChart.tsx
@@ -1,4 +1,4 @@
import React, { Component, cloneElement, isValidElement, createElement, ReactElement } from 'react';
import React, { Component, cloneElement, isValidElement, ReactElement } from 'react';
import isNil from 'lodash/isNil';
import isFunction from 'lodash/isFunction';
import range from 'lodash/range';
Expand All @@ -12,17 +12,13 @@
// eslint-disable-next-line no-restricted-imports
import type { DebouncedFunc } from 'lodash';
import invariant from 'tiny-invariant';
import { getRadialCursorPoints } from '../util/cursor/getRadialCursorPoints';
import { getTicks } from '../cartesian/getTicks';
import { Surface } from '../container/Surface';
import { Layer } from '../container/Layer';
import { Tooltip } from '../component/Tooltip';
import { Legend } from '../component/Legend';
import { Curve } from '../shape/Curve';
import { Cross } from '../shape/Cross';
import { Sector } from '../shape/Sector';
import { Dot } from '../shape/Dot';
import { isInRectangle, Rectangle } from '../shape/Rectangle';
import { isInRectangle } from '../shape/Rectangle';

import {
filterProps,
Expand Down Expand Up @@ -89,8 +85,8 @@
import { getActiveShapeIndexForTooltip, isFunnel, isPie, isScatter } from '../util/ActiveShapeUtils';
import { Props as YAxisProps } from '../cartesian/YAxis';
import { Props as XAxisProps } from '../cartesian/XAxis';
import { getCursorPoints } from '../util/cursor/getCursorPoints';
import { getCursorRectangle } from '../util/cursor/getCursorRectangle';
import { TooltipRenderer } from '../renderer/TooltipRenderer';
import { ChartContextContainer } from '../context/chartContext';

export interface MousePointer {
pageX: number;
Expand All @@ -113,6 +109,22 @@

const originCoordinate: Coordinate = { x: 0, y: 0 };

/**
* This function exists as a temporary workaround.
*
* Why? generateCategoricalChart does not render `{children}` directly;
* instead it passes them through `renderByOrder` function which reads their handlers.
*
* So, this is a handler that does nothing.
* Once we get rid of `renderByOrder` and switch to JSX only, we can get rid of this handler too.
*
* @param {JSX} element as is in JSX
* @returns {JSX} the same element
*/
function renderAsIs(element: React.ReactElement): React.ReactElement {
return element;
}

const calculateTooltipPos = (rangeObj: any, layout: LayoutType): any => {
if (layout === 'horizontal') {
return rangeObj.x;
Expand Down Expand Up @@ -1751,63 +1763,6 @@
return null;
}

renderCursor = (element: ReactElement) => {
const { isTooltipActive, activeCoordinate, activePayload, offset, activeTooltipIndex, tooltipAxisBandSize } =
this.state;
const tooltipEventType = this.getTooltipEventType();

if (
!element ||
!element.props.cursor ||
!isTooltipActive ||
!activeCoordinate ||
(chartName !== 'ScatterChart' && tooltipEventType !== 'axis')
) {
return null;
}
const { layout } = this.props;
let restProps;
let cursorComp: React.ComponentType<any> = Curve;

if (chartName === 'ScatterChart') {
restProps = activeCoordinate;
cursorComp = Cross;
} else if (chartName === 'BarChart') {
restProps = getCursorRectangle(layout, activeCoordinate, offset, tooltipAxisBandSize);
cursorComp = Rectangle;
} else if (layout === 'radial') {
const { cx, cy, radius, startAngle, endAngle } = getRadialCursorPoints(activeCoordinate);
restProps = {
cx,
cy,
startAngle,
endAngle,
innerRadius: radius,
outerRadius: radius,
};
cursorComp = Sector;
} else {
restProps = { points: getCursorPoints(layout, activeCoordinate, offset) };
cursorComp = Curve;
}
const key = element.key || '_recharts-cursor';
const cursorProps = {
stroke: '#ccc',
pointerEvents: 'none',
...offset,
...restProps,
...filterProps(element.props.cursor),
payload: activePayload,
payloadIndex: activeTooltipIndex,
key,
className: 'recharts-tooltip-cursor',
};

return isValidElement(element.props.cursor)
? cloneElement(element.props.cursor, cursorProps)
: createElement(cursorComp, cursorProps);
};

renderPolarAxis = (element: any, displayName: string, index: number) => {
const axisType = get(element, 'type.axisType');
const axisMap = get(this.state, `${axisType}Map`);
Expand Down Expand Up @@ -1940,27 +1895,25 @@
});
};

/**
* Draw Tooltip
* @return {ReactElement} The instance of Tooltip
*/
renderTooltip = (): React.ReactElement => {
const { children } = this.props;
const tooltipItem = findChildByType(children, Tooltip);

if (!tooltipItem) {
return null;
}

const { isTooltipActive, activeCoordinate, activePayload, activeLabel, offset } = this.state;
const { activeCoordinate, activePayload, offset, isTooltipActive, activeTooltipIndex, activeLabel } = this.state;

return cloneElement(tooltipItem, {
viewBox: { ...offset, x: offset.left, y: offset.top },
active: isTooltipActive,
label: activeLabel,
payload: isTooltipActive ? activePayload : [],
coordinate: activeCoordinate,
});
const { layout } = this.props;
return (
<TooltipRenderer
ckifer marked this conversation as resolved.
Show resolved Hide resolved
activeCoordinate={activeCoordinate}
chartName={chartName}
layout={layout}
offset={offset}
viewBox={{ ...offset, x: offset.left, y: offset.top }}
tooltipAxisBandSize={0}
activeTooltipIndex={activeTooltipIndex}
activeLabel={activeLabel}
activePayload={activePayload}
isTooltipActive={isTooltipActive}
tooltipEventType={this.getTooltipEventType()}
/>
);
};

renderBrush = (element: React.ReactElement) => {
Expand Down Expand Up @@ -2277,11 +2230,11 @@
Scatter: { handler: this.renderGraphicChild },
Pie: { handler: this.renderGraphicChild },
Funnel: { handler: this.renderGraphicChild },
Tooltip: { handler: this.renderCursor, once: true },
PolarGrid: { handler: this.renderPolarGrid, once: true },
PolarAngleAxis: { handler: this.renderPolarAxis },
PolarRadiusAxis: { handler: this.renderPolarAxis },
Customized: { handler: this.renderCustomized },
Tooltip: { handler: renderAsIs },
};

render() {
Expand Down Expand Up @@ -2321,22 +2274,24 @@

const events = this.parseEventsOfWrapper();
return (
<div
className={clsx('recharts-wrapper', className)}
style={{ position: 'relative', cursor: 'default', width, height, ...style }}
{...events}
ref={(node: HTMLDivElement) => {
this.container = node;
}}
role="region"
>
<Surface {...attrs} width={width} height={height} title={title} desc={desc} style={FULL_WIDTH_AND_HEIGHT}>
{this.renderClipPath()}
{renderByOrder(children, this.renderMap)}
</Surface>
{this.renderLegend()}
{this.renderTooltip()}
</div>
<ChartContextContainer>
<div
className={clsx('recharts-wrapper', className)}
style={{ position: 'relative', cursor: 'default', width, height, ...style }}
{...events}
ref={(node: HTMLDivElement) => {
this.container = node;
}}
role="region"
>
<Surface {...attrs} width={width} height={height} title={title} desc={desc}>
{this.renderClipPath()}
{renderByOrder(children, map)}

Check failure on line 2289 in src/chart/generateCategoricalChart.tsx

View workflow job for this annotation

GitHub Actions / Build and Test on 16.x

Cannot find name 'map'. Did you mean 'Map'?

Check failure on line 2289 in src/chart/generateCategoricalChart.tsx

View workflow job for this annotation

GitHub Actions / Build and Test on 18.x

Cannot find name 'map'. Did you mean 'Map'?

Check failure on line 2289 in src/chart/generateCategoricalChart.tsx

View workflow job for this annotation

GitHub Actions / Build and Test on 20.x

Cannot find name 'map'. Did you mean 'Map'?
</Surface>
{this.renderLegend()}
{this.renderTooltip()}
</div>
</ChartContextContainer>
);
}
};
Expand Down
116 changes: 38 additions & 78 deletions src/component/Tooltip.tsx
@@ -1,43 +1,19 @@
/**
* @fileOverview Tooltip
*/
import React, { PureComponent, CSSProperties, ReactNode, ReactElement, SVGProps } from 'react';
import {
DefaultTooltipContent,
ValueType,
NameType,
Payload,
Props as ToltipContentProps,
} from './DefaultTooltipContent';
import { TooltipBoundingBox } from './TooltipBoundingBox';
import React, { PureComponent, CSSProperties, ReactNode, ReactElement, SVGProps, useContext, useEffect } from 'react';
import { ValueType, NameType, Payload, Props as TooltipContentProps } from './DefaultTooltipContent';

import { Global } from '../util/Global';
import { UniqueOption, getUniqPayload } from '../util/payload/getUniqPayload';
import { UniqueOption } from '../util/payload/getUniqPayload';
import { AllowInDimension, AnimationDuration, AnimationTiming, CartesianViewBox, Coordinate } from '../util/types';
import { ChartContext, ChartContextType } from '../context/chartContext';

export type ContentType<TValue extends ValueType, TName extends NameType> =
| ReactElement
| ((props: TooltipProps<TValue, TName>) => ReactNode);

function defaultUniqBy<TValue extends ValueType, TName extends NameType>(entry: Payload<TValue, TName>) {
return entry.dataKey;
}

function renderContent<TValue extends ValueType, TName extends NameType>(
content: ContentType<TValue, TName>,
props: TooltipProps<TValue, TName>,
): ReactNode {
if (React.isValidElement(content)) {
return React.cloneElement(content, props);
}
if (typeof content === 'function') {
return React.createElement(content as any, props);
}

return <DefaultTooltipContent {...props} />;
}

export type TooltipProps<TValue extends ValueType, TName extends NameType> = ToltipContentProps<TValue, TName> & {
export type TooltipProps<TValue extends ValueType, TName extends NameType> = TooltipContentProps<TValue, TName> & {
active?: boolean;
allowEscapeViewBox?: AllowInDimension;
animationDuration?: AnimationDuration;
Expand All @@ -58,6 +34,38 @@ export type TooltipProps<TValue extends ValueType, TName extends NameType> = Tol
wrapperStyle?: CSSProperties;
};

function ContextUpdater(props: TooltipProps<any, any>) {
const [, setContext] = useContext(ChartContext);
useEffect(() => {
const newContext: ChartContextType = {
isTooltipPresent: true,
content: props.content,
cursor: props.cursor,
isAnimationActive: props.isAnimationActive,
animationDuration: props.animationDuration,
animationEasing: props.animationEasing,
filterNull: props.filterNull,
// @ts-expect-error TODO this function will refine from the "outside, flexible prop" to "inside, strict prop" type
payloadUniqBy: props.payloadUniqBy,
wrapperStyle: props.wrapperStyle,
useTranslate3d: props.useTranslate3d,
// @ts-expect-error TODO
allowEscapeViewBox: props.allowEscapeViewBox,
// @ts-expect-error TODO
reverseDirection: props.reverseDirection,
offsetTopLeft: props.offset,
// @ts-expect-error TODO
position: props.position,
};
setContext(newContext);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The public Tooltip will now only gather and cleanup the public props and push to context - nothing more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should scrutinize every effect we add, but I guess optimizations can come later once its working

return function cleanup() {
setContext({ isTooltipPresent: false });
};
}, [props, setContext]);

return <></>;
}

export class Tooltip<TValue extends ValueType, TName extends NameType> extends PureComponent<
TooltipProps<TValue, TName>
> {
Expand Down Expand Up @@ -86,54 +94,6 @@ export class Tooltip<TValue extends ValueType, TName extends NameType> extends P
};

render() {
const {
active,
allowEscapeViewBox,
animationDuration,
animationEasing,
content,
coordinate,
filterNull,
isAnimationActive,
offset,
payload,
payloadUniqBy,
position,
reverseDirection,
useTranslate3d,
viewBox,
wrapperStyle,
} = this.props;
let finalPayload: Payload<TValue, TName>[] = payload ?? [];

if (filterNull && finalPayload.length) {
finalPayload = getUniqPayload(
payload.filter(entry => entry.value != null),
payloadUniqBy,
defaultUniqBy,
);
}

const hasPayload = finalPayload.length > 0;

return (
<TooltipBoundingBox
allowEscapeViewBox={allowEscapeViewBox}
animationDuration={animationDuration}
animationEasing={animationEasing}
isAnimationActive={isAnimationActive}
active={active}
coordinate={coordinate}
hasPayload={hasPayload}
offset={offset}
position={position}
reverseDirection={reverseDirection}
useTranslate3d={useTranslate3d}
viewBox={viewBox}
wrapperStyle={wrapperStyle}
>
{renderContent(content, { ...this.props, payload: finalPayload })}
</TooltipBoundingBox>
);
return <ContextUpdater {...this.props} />;
}
}