From 7841001b3c88df621361f638411f359af9cbab92 Mon Sep 17 00:00:00 2001 From: Nathan Knight Date: Tue, 12 Jul 2022 09:33:17 -0400 Subject: [PATCH] feat: Eliminated unsafe lifecycle methods, rewrote GenericComponent and GenericChartComponent --- packages/annotations/src/Label.tsx | 2 +- packages/axes/src/Axis.tsx | 21 +- packages/core/src/Chart.tsx | 8 +- packages/core/src/ChartCanvas.tsx | 258 ++++--- packages/core/src/EventCapture.tsx | 29 +- packages/core/src/GenericChartComponent.tsx | 211 +++--- packages/core/src/GenericComponent.tsx | 675 +++++++++--------- packages/core/src/useEvent.ts | 14 + packages/core/src/utils/ChartDataUtil.ts | 4 +- packages/core/src/utils/index.ts | 3 +- packages/series/src/AreaSeries.tsx | 2 +- packages/tooltip/src/MovingAverageTooltip.tsx | 13 +- 12 files changed, 645 insertions(+), 595 deletions(-) create mode 100644 packages/core/src/useEvent.ts diff --git a/packages/annotations/src/Label.tsx b/packages/annotations/src/Label.tsx index 41b50967..fe025395 100644 --- a/packages/annotations/src/Label.tsx +++ b/packages/annotations/src/Label.tsx @@ -58,7 +58,7 @@ export class Label extends React.Component { const { xScale, chartConfig, xAccessor } = moreProps; - const yScale = Array.isArray(chartConfig) ? undefined : chartConfig.yScale; + const yScale = Array.isArray(chartConfig) || !chartConfig ? undefined : chartConfig.yScale; const { xPos, yPos, fillStyle, text } = this.helper(moreProps, xAccessor, xScale, yScale); diff --git a/packages/axes/src/Axis.tsx b/packages/axes/src/Axis.tsx index e710772b..9da6afa3 100644 --- a/packages/axes/src/Axis.tsx +++ b/packages/axes/src/Axis.tsx @@ -1,7 +1,8 @@ import { first, - getAxisCanvas, GenericChartComponent, + GenericComponentRef, + getAxisCanvas, getStrokeDasharrayCanvas, last, strokeDashTypes, @@ -57,6 +58,16 @@ interface AxisProps { readonly zoomCursorClassName?: string; } +interface Tick { + value: number; + x1: number; + y1: number; + x2: number; + y2: number; + labelX: number; + labelY: number; +} + export class Axis extends React.Component { public static defaultProps = { edgeClip: false, @@ -64,7 +75,7 @@ export class Axis extends React.Component { zoomCursorClassName: "", }; - private readonly chartRef = React.createRef(); + private readonly chartRef = React.createRef(); public render() { const { @@ -138,7 +149,7 @@ export class Axis extends React.Component { } if (showGridLines) { - tickProps.ticks.forEach((tick: any) => { + tickProps.ticks.forEach((tick) => { drawGridLine(ctx, tick, tickProps, moreProps); }); } @@ -209,7 +220,7 @@ const tickHelper = (props: AxisProps, scale: ScaleContinuousNumeric { }); }; -const drawGridLine = (ctx: CanvasRenderingContext2D, tick: any, result: any, moreProps: any) => { +const drawGridLine = (ctx: CanvasRenderingContext2D, tick: Tick, result: any, moreProps: any) => { const { orient, gridLinesStrokeWidth, gridLinesStrokeStyle, gridLinesStrokeDasharray } = result; const { chartConfig } = moreProps; diff --git a/packages/core/src/Chart.tsx b/packages/core/src/Chart.tsx index 130ba243..6c3c32d2 100644 --- a/packages/core/src/Chart.tsx +++ b/packages/core/src/Chart.tsx @@ -48,7 +48,7 @@ export const Chart = React.memo((props: React.PropsWithChildren) => } = props; const chartCanvasContextValue = React.useContext(ChartCanvasContext); - const { subscribe, unsubscribe, chartConfig } = chartCanvasContextValue; + const { subscribe, unsubscribe, chartConfigs } = chartCanvasContextValue; const listener = React.useCallback( (type: string, moreProps: any, _: any, e: React.MouseEvent) => { @@ -89,7 +89,7 @@ export const Chart = React.memo((props: React.PropsWithChildren) => return () => unsubscribe(`chart_${id}`); }, [subscribe, unsubscribe, id, listener]); - const config = chartConfig.find(({ id }) => id === props.id)!; + const config = chartConfigs.find(({ id }) => id === props.id)!; const contextValue = React.useMemo(() => { return { ...chartCanvasContextValue, @@ -104,7 +104,9 @@ export const Chart = React.memo((props: React.PropsWithChildren) => return ( - {props.children} + + {props.children} + ); }); diff --git a/packages/core/src/ChartCanvas.tsx b/packages/core/src/ChartCanvas.tsx index 9460b6b6..234714e2 100644 --- a/packages/core/src/ChartCanvas.tsx +++ b/packages/core/src/ChartCanvas.tsx @@ -11,8 +11,9 @@ import { getNewChartConfig, } from "./utils/ChartDataUtil"; import { EventCapture } from "./EventCapture"; -import { CanvasContainer } from "./CanvasContainer"; +import { CanvasContainer, ICanvasContexts } from "./CanvasContainer"; import evaluator from "./utils/evaluator"; +import { MoreProps } from "./GenericComponent"; const CANDIDATES_FOR_RESET = ["seriesName"]; @@ -70,40 +71,46 @@ const getCursorStyle = () => { export interface ChartCanvasContextType { width: number; height: number; - margin: {}; + margin: { top: number; right: number; bottom: number; left: number }; chartId: number | string; - getCanvasContexts?: () => void; + getCanvasContexts?: () => ICanvasContexts | undefined; xScale: Function; + ratio: number; // Not sure if it should be optional xAccessor: (data: any) => TXAxis; displayXAccessor: (data: any) => TXAxis; + xAxisZoom?: (newDomain: any) => void; + yAxisZoom?: (chartId: string, newDomain: any) => void; + redraw: () => void; plotData: any[]; fullData: any[]; - chartConfig: ChartConfig[]; + chartConfigs: ChartConfig[]; morePropsDecorator?: () => void; - generateSubscriptionId?: () => void; + generateSubscriptionId?: () => number; getMutableState: () => {}; - amIOnTop: (id: string) => boolean; - subscribe: (id: string, rest: any) => void; - unsubscribe: (id: string) => void; - setCursorClass: (className: string) => void; + amIOnTop: (id: string | number) => boolean; + subscribe: (id: string | number, rest: any) => void; + unsubscribe: (id: string | number) => void; + setCursorClass: (className: string | null | undefined) => void; } // eslint-disable-next-line @typescript-eslint/no-empty-function const noop = () => {}; export const chartCanvasContextDefaultValue: ChartCanvasContextType = { amIOnTop: () => false, - chartConfig: [], + chartConfigs: [], chartId: 0, + ratio: 0, displayXAccessor: () => 0, fullData: [], getMutableState: () => ({}), height: 0, - margin: {}, + margin: { top: 0, right: 0, bottom: 0, left: 0 }, plotData: [], setCursorClass: noop, subscribe: noop, unsubscribe: noop, + redraw: noop, width: 0, xAccessor: () => 0, xScale: noop, @@ -168,7 +175,7 @@ const resetChart = (props: ChartCanvasProps(props: ChartCanvasProps(props: ChartCanvasProps) => { - const { xAccessor: inputXAccesor, xExtents: xExtentsProp, data, padding, flipXScale } = props; + const { xAccessor: inputXAccessor, xExtents: xExtentsProp, data, padding, flipXScale } = props; const direction = getXScaleDirection(flipXScale); @@ -264,14 +271,14 @@ const calculateState = (props: ChartCanvasProps( - xExtentsProp.map((d: any) => functor(d)).map((each: any) => each(data, inputXAccesor)), + xExtentsProp.map((d: any) => functor(d)).map((each: any) => each(data, inputXAccessor)), ) as [TXAxis, TXAxis]); const { xAccessor, displayXAccessor, xScale, fullData, filterData } = calculateFullData(props); const updatedXScale = setXRange(xScale, dimensions, padding, direction); - const { plotData, domain } = filterData(fullData, extent, inputXAccesor, updatedXScale); + const { plotData, domain } = filterData(fullData, extent, inputXAccessor, updatedXScale); return { plotData, @@ -393,12 +400,28 @@ interface ChartCanvasState { xAccessor: (data: any) => TXAxis; displayXAccessor?: any; filterData?: any; - chartConfig: ChartConfig[]; + chartConfigs: ChartConfig[]; plotData: any[]; xScale: ScaleContinuousNumeric | ScaleTime; fullData: any[]; } +interface Subscription { + id: string; + getPanConditions: () => { + draggable: boolean; + panEnabled: boolean; + }; + draw: (props: { trigger: string } | { force: boolean }) => void; + listener: (type: string, newMoreProps: MoreProps | undefined, state: any, e: any) => void; +} + +interface MutableState { + mouseXY: [number, number]; + currentItem: any; + currentCharts: string[]; +} + export class ChartCanvas extends React.Component< ChartCanvasProps, ChartCanvasState @@ -430,10 +453,10 @@ export class ChartCanvas extends React.Component< private readonly eventCaptureRef = React.createRef(); private finalPinch?: boolean; private lastSubscriptionId = 0; - private mutableState = {}; + private mutableState: MutableState = { mouseXY: [0, 0], currentCharts: [], currentItem: null }; private panInProgress = false; private prevMouseXY?: number[]; - private subscriptions: any[] = []; + private subscriptions: Subscription[] = []; private waitingForPinchZoomAnimationFrame?: boolean; private waitingForPanAnimationFrame?: boolean; private waitingForMouseMoveAnimationFrame?: boolean; @@ -452,7 +475,7 @@ export class ChartCanvas extends React.Component< props: ChartCanvasProps, state: ChartCanvasState, ): ChartCanvasState { - const { chartConfig: initialChartConfig, plotData, xAccessor, xScale } = state; + const { chartConfigs: initialChartConfig, plotData, xAccessor, xScale } = state; const interaction = isInteractionEnabled(xScale, xAccessor, plotData); const shouldReset = shouldResetChart(state.lastProps || {}, props); let newState: ChartCanvasState; @@ -527,7 +550,7 @@ export class ChartCanvas extends React.Component< } } - public subscribe = (id: string, rest: any) => { + public subscribe = (id: string | number, rest: any) => { const { getPanConditions = functor({ draggable: false, @@ -542,7 +565,7 @@ export class ChartCanvas extends React.Component< }); }; - public unsubscribe = (id: string) => { + public unsubscribe = (id: string | number) => { this.subscriptions = this.subscriptions.filter((each) => each.id !== id); }; @@ -550,20 +573,20 @@ export class ChartCanvas extends React.Component< return this.subscriptions.map((each) => each.getPanConditions()); }; - public setCursorClass = (className: string) => { + public setCursorClass = (className: string | null | undefined) => { this.eventCaptureRef.current?.setCursorClass(className); }; - public amIOnTop = (id: string) => { + public amIOnTop = (id: string | number) => { const dragableComponents = this.subscriptions.filter((each) => each.getPanConditions().draggable); return dragableComponents.length > 0 && last(dragableComponents).id === id; }; public handleContextMenu = (mouseXY: number[], e: React.MouseEvent) => { - const { xAccessor, chartConfig, plotData, xScale } = this.state; + const { xAccessor, chartConfigs, plotData, xScale } = this.state; - const currentCharts = getCurrentCharts(chartConfig, mouseXY); + const currentCharts = getCurrentCharts(chartConfigs, mouseXY); const currentItem = getCurrentItem(xScale, xAccessor, mouseXY, plotData); @@ -583,7 +606,7 @@ export class ChartCanvas extends React.Component< xAccessor, displayXAccessor, xScale: initialXScale, - chartConfig: initialChartConfig, + chartConfigs: initialChartConfig, plotData: initialPlotData, } = this.state; @@ -601,7 +624,7 @@ export class ChartCanvas extends React.Component< | ScaleContinuousNumeric | ScaleTime; - const chartConfig = getChartConfigWithUpdatedYScales( + const chartConfigs = getChartConfigWithUpdatedYScales( initialChartConfig, { plotData, xAccessor, displayXAccessor, fullData }, updatedScale.domain(), @@ -610,7 +633,7 @@ export class ChartCanvas extends React.Component< return { xScale: updatedScale, plotData, - chartConfig, + chartConfigs, }; }; @@ -619,7 +642,7 @@ export class ChartCanvas extends React.Component< const { xScale: initialXScale, - chartConfig: initialChartConfig, + chartConfigs: initialChartConfig, plotData: initialPlotData, xAccessor, displayXAccessor, @@ -656,7 +679,7 @@ export class ChartCanvas extends React.Component< const mouseXY = finalPinch.touch1Pos; - const chartConfig = getChartConfigWithUpdatedYScales( + const chartConfigs = getChartConfigWithUpdatedYScales( initialChartConfig, { plotData, xAccessor, displayXAccessor, fullData }, updatedScale.domain(), @@ -665,7 +688,7 @@ export class ChartCanvas extends React.Component< const currentItem = getCurrentItem(updatedScale, xAccessor, mouseXY, plotData); return { - chartConfig, + chartConfigs, xScale: updatedScale, plotData, mouseXY, @@ -758,10 +781,10 @@ export class ChartCanvas extends React.Component< .map((x) => cx + (x - cx) * c) .map((x) => initialXScale.invert(x)); - const { xScale, plotData, chartConfig } = this.calculateStateForDomain(newDomain); + const { xScale, plotData, chartConfigs } = this.calculateStateForDomain(newDomain); const currentItem = getCurrentItem(xScale, xAccessor, mouseXY, plotData); - const currentCharts = getCurrentCharts(chartConfig, mouseXY); + const currentCharts = getCurrentCharts(chartConfigs, mouseXY); this.clearThreeCanvas(); @@ -784,7 +807,7 @@ export class ChartCanvas extends React.Component< { xScale, plotData, - chartConfig, + chartConfigs, mouseXY, currentCharts, currentItem, @@ -799,7 +822,7 @@ export class ChartCanvas extends React.Component< { xScale, plotData, - chartConfig, + chartConfigs, }, () => { if (scale_start < data_start) { @@ -817,7 +840,7 @@ export class ChartCanvas extends React.Component< }; public xAxisZoom = (newDomain: any) => { - const { xScale, plotData, chartConfig } = this.calculateStateForDomain(newDomain); + const { xScale, plotData, chartConfigs } = this.calculateStateForDomain(newDomain); this.clearThreeCanvas(); const { xAccessor, fullData } = this.state; @@ -835,7 +858,7 @@ export class ChartCanvas extends React.Component< { xScale, plotData, - chartConfig, + chartConfigs, }, () => { if (scale_start < data_start) { @@ -854,8 +877,8 @@ export class ChartCanvas extends React.Component< public yAxisZoom = (chartId: string, newDomain: any) => { this.clearThreeCanvas(); - const { chartConfig: initialChartConfig } = this.state; - const chartConfig = initialChartConfig.map((each: any) => { + const { chartConfigs: initialChartConfig } = this.state; + const chartConfigs = initialChartConfig.map((each: any) => { if (each.id === chartId) { const { yScale } = each; return { @@ -869,7 +892,7 @@ export class ChartCanvas extends React.Component< }); this.setState({ - chartConfig, + chartConfigs, }); }; @@ -883,7 +906,7 @@ export class ChartCanvas extends React.Component< }); } - public draw = (props: any) => { + public draw = (props: { trigger: string } | { force: boolean }) => { this.subscriptions.forEach((each) => { if (isDefined(each.draw)) { each.draw(props); @@ -897,12 +920,12 @@ export class ChartCanvas extends React.Component< }; public panHelper = ( - mouseXY: number[], + mouseXY: [number, number], initialXScale: ScaleContinuousNumeric | ScaleTime, { dx, dy }: { dx: number; dy: number }, chartsToPan: string[], ) => { - const { xAccessor, displayXAccessor, chartConfig: initialChartConfig, filterData, fullData } = this.state; + const { xAccessor, displayXAccessor, chartConfigs: initialChartConfig, filterData, fullData } = this.state; const { postCalculator = ChartCanvas.defaultProps.postCalculator } = this.props; const newDomain = initialXScale @@ -924,7 +947,7 @@ export class ChartCanvas extends React.Component< const currentItem = getCurrentItem(updatedScale, xAccessor, mouseXY, plotData); - const chartConfig = getChartConfigWithUpdatedYScales( + const chartConfigs = getChartConfigWithUpdatedYScales( initialChartConfig, { plotData, xAccessor, displayXAccessor, fullData }, updatedScale.domain(), @@ -932,12 +955,12 @@ export class ChartCanvas extends React.Component< chartsToPan, ); - const currentCharts = getCurrentCharts(chartConfig, mouseXY); + const currentCharts = getCurrentCharts(chartConfigs, mouseXY); return { xScale: updatedScale, plotData, - chartConfig, + chartConfigs, mouseXY, currentCharts, currentItem, @@ -945,44 +968,45 @@ export class ChartCanvas extends React.Component< }; public handlePan = ( - mousePosition: number[], + mousePosition: [number, number], panStartXScale: ScaleContinuousNumeric | ScaleTime, dxdy: { dx: number; dy: number }, chartsToPan: string[], e: React.MouseEvent, ) => { - if (!this.waitingForPanAnimationFrame) { - this.waitingForPanAnimationFrame = true; + if (this.waitingForPanAnimationFrame) { + return; + } + this.waitingForPanAnimationFrame = true; - this.hackyWayToStopPanBeyondBounds__plotData = - this.hackyWayToStopPanBeyondBounds__plotData ?? this.state.plotData; - this.hackyWayToStopPanBeyondBounds__domain = - this.hackyWayToStopPanBeyondBounds__domain ?? this.state.xScale!.domain(); + this.hackyWayToStopPanBeyondBounds__plotData = + this.hackyWayToStopPanBeyondBounds__plotData ?? this.state.plotData; + this.hackyWayToStopPanBeyondBounds__domain = + this.hackyWayToStopPanBeyondBounds__domain ?? this.state.xScale!.domain(); - const newState = this.panHelper(mousePosition, panStartXScale, dxdy, chartsToPan); + const newState = this.panHelper(mousePosition, panStartXScale, dxdy, chartsToPan); - this.hackyWayToStopPanBeyondBounds__plotData = newState.plotData; - this.hackyWayToStopPanBeyondBounds__domain = newState.xScale.domain(); + this.hackyWayToStopPanBeyondBounds__plotData = newState.plotData; + this.hackyWayToStopPanBeyondBounds__domain = newState.xScale.domain(); - this.panInProgress = true; + this.panInProgress = true; - this.triggerEvent("pan", newState, e); + this.triggerEvent("pan", newState, e); - this.mutableState = { - mouseXY: newState.mouseXY, - currentItem: newState.currentItem, - currentCharts: newState.currentCharts, - }; - requestAnimationFrame(() => { - this.waitingForPanAnimationFrame = false; - this.clearBothCanvas(); - this.draw({ trigger: "pan" }); - }); - } + this.mutableState = { + mouseXY: newState.mouseXY, + currentItem: newState.currentItem, + currentCharts: newState.currentCharts, + }; + requestAnimationFrame(() => { + this.waitingForPanAnimationFrame = false; + this.clearBothCanvas(); + this.draw({ trigger: "pan" }); + }); }; public handlePanEnd = ( - mousePosition: number[], + mousePosition: [number, number], panStartXScale: ScaleContinuousNumeric | ScaleTime, dxdy: { dx: number; dy: number }, chartsToPan: string[], @@ -994,7 +1018,7 @@ export class ChartCanvas extends React.Component< this.panInProgress = false; - const { xScale, plotData, chartConfig } = state; + const { xScale, plotData, chartConfigs } = state; this.triggerEvent("panend", state, e); @@ -1017,7 +1041,7 @@ export class ChartCanvas extends React.Component< { xScale, plotData, - chartConfig, + chartConfigs, }, () => { if (scale_start < data_start) { @@ -1049,40 +1073,37 @@ export class ChartCanvas extends React.Component< ); }; - public handleMouseMove = (mouseXY: number[], _: string, e: any) => { - if (!this.waitingForMouseMoveAnimationFrame) { - this.waitingForMouseMoveAnimationFrame = true; - - const { chartConfig, plotData, xScale, xAccessor } = this.state; - - const currentCharts = getCurrentCharts(chartConfig, mouseXY); - const currentItem = getCurrentItem(xScale, xAccessor, mouseXY, plotData); - this.triggerEvent( - "mousemove", - { - show: true, - mouseXY, - // prevMouseXY is used in interactive components - prevMouseXY: this.prevMouseXY, - currentItem, - currentCharts, - }, - e, - ); - - this.prevMouseXY = mouseXY; - this.mutableState = { + public handleMouseMove = (mouseXY: [number, number], _: string, e: any) => { + if (this.waitingForMouseMoveAnimationFrame) { + return; + } + this.waitingForMouseMoveAnimationFrame = true; + const { chartConfigs, plotData, xScale, xAccessor } = this.state; + const currentCharts = getCurrentCharts(chartConfigs, mouseXY); + const currentItem = getCurrentItem(xScale, xAccessor, mouseXY, plotData); + this.triggerEvent( + "mousemove", + { + show: true, mouseXY, + // prevMouseXY is used in interactive components + prevMouseXY: this.prevMouseXY, currentItem, currentCharts, - }; - - requestAnimationFrame(() => { - this.clearMouseCanvas(); - this.draw({ trigger: "mousemove" }); - this.waitingForMouseMoveAnimationFrame = false; - }); - } + }, + e, + ); + this.prevMouseXY = mouseXY; + this.mutableState = { + mouseXY, + currentItem, + currentCharts, + }; + requestAnimationFrame(() => { + this.clearMouseCanvas(); + this.draw({ trigger: "mousemove" }); + this.waitingForMouseMoveAnimationFrame = false; + }); }; public handleMouseLeave = (e: any) => { @@ -1095,10 +1116,13 @@ export class ChartCanvas extends React.Component< this.triggerEvent("dragstart", { startPos }, e); }; - public handleDrag = ({ startPos, mouseXY }: { startPos: number[]; mouseXY: number[] }, e: React.MouseEvent) => { - const { chartConfig, plotData, xScale, xAccessor } = this.state; + public handleDrag = ( + { startPos, mouseXY }: { startPos: [number, number]; mouseXY: [number, number] }, + e: React.MouseEvent, + ) => { + const { chartConfigs, plotData, xScale, xAccessor } = this.state; - const currentCharts = getCurrentCharts(chartConfig, mouseXY); + const currentCharts = getCurrentCharts(chartConfigs, mouseXY); const currentItem = getCurrentItem(xScale, xAccessor, mouseXY, plotData); this.triggerEvent( @@ -1147,7 +1171,7 @@ export class ChartCanvas extends React.Component< }; // TODO: Memoize this - public getContextValues() { + public getContextValues(): ChartCanvasContextType { const dimensions = getDimensions(this.props); return { chartId: -1, @@ -1155,7 +1179,7 @@ export class ChartCanvas extends React.Component< plotData: this.state.plotData, width: dimensions.width, height: dimensions.height, - chartConfig: this.state.chartConfig, + chartConfigs: this.state.chartConfigs, xScale: this.state.xScale, xAccessor: this.state.xAccessor, displayXAccessor: this.state.displayXAccessor, @@ -1175,9 +1199,9 @@ export class ChartCanvas extends React.Component< } public resetYDomain = (chartId?: string) => { - const { chartConfig } = this.state; + const { chartConfigs } = this.state; let changed = false; - const newChartConfig = chartConfig.map((each: any) => { + const newChartConfig = chartConfigs.map((each: any) => { if ( (isNotDefined(chartId) || each.id === chartId) && !shallowEqual(each.yScale.domain(), each.realYDomain) @@ -1195,7 +1219,7 @@ export class ChartCanvas extends React.Component< if (changed) { this.clearThreeCanvas(); this.setState({ - chartConfig: newChartConfig, + chartConfigs: newChartConfig, }); } }; @@ -1222,7 +1246,7 @@ export class ChartCanvas extends React.Component< mouseMoveEvent, } = this.props; - const { plotData, xScale, xAccessor, chartConfig } = this.state; + const { plotData, xScale, xAccessor, chartConfigs } = this.state; const dimensions = getDimensions(this.props); @@ -1258,7 +1282,7 @@ export class ChartCanvas extends React.Component< - {chartConfig.map((each: any, idx: number) => ( + {chartConfigs.map((each: any, idx: number) => ( @@ -1273,7 +1297,7 @@ export class ChartCanvas extends React.Component< pan={!disablePan && interaction} width={dimensions.width} height={dimensions.height} - chartConfig={chartConfig} + chartConfig={chartConfigs} xScale={xScale!} xAccessor={xAccessor} focus={defaultFocus} diff --git a/packages/core/src/EventCapture.tsx b/packages/core/src/EventCapture.tsx index f584f7fa..012b4673 100644 --- a/packages/core/src/EventCapture.tsx +++ b/packages/core/src/EventCapture.tsx @@ -14,7 +14,7 @@ import { TOUCHMOVE, touchPosition, } from "./utils"; -import { getCurrentCharts } from "./utils/ChartDataUtil"; +import { ChartConfig, getCurrentCharts } from "./utils/ChartDataUtil"; interface EventCaptureProps { readonly mouseMove: boolean; @@ -25,7 +25,7 @@ interface EventCaptureProps { readonly useCrossHairStyleCursor?: boolean; readonly width: number; readonly height: number; - readonly chartConfig: { origin: number[]; height: number }[]; + readonly chartConfig: ChartConfig[]; readonly xAccessor: any; // func readonly xScale: ScaleContinuousNumeric | ScaleTime; readonly disableInteraction: boolean; @@ -34,10 +34,17 @@ interface EventCaptureProps { readonly onContextMenu?: (mouseXY: number[], event: React.MouseEvent) => void; readonly onDoubleClick?: (mouseXY: number[], event: React.MouseEvent) => void; readonly onDragStart?: (details: { startPos: number[] }, event: React.MouseEvent) => void; - readonly onDrag?: (details: { startPos: number[]; mouseXY: number[] }, event: React.MouseEvent) => void; + readonly onDrag?: ( + details: { startPos: [number, number]; mouseXY: [number, number] }, + event: React.MouseEvent, + ) => void; readonly onDragComplete?: (details: { mouseXY: number[] }, event: React.MouseEvent) => void; - readonly onMouseDown?: (mouseXY: number[], currentCharts: string[], event: React.MouseEvent) => void; - readonly onMouseMove?: (touchXY: number[], eventType: string, event: React.MouseEvent | React.TouchEvent) => void; + readonly onMouseDown?: (mouseXY: [number, number], currentCharts: string[], event: React.MouseEvent) => void; + readonly onMouseMove?: ( + touchXY: [number, number], + eventType: string, + event: React.MouseEvent | React.TouchEvent, + ) => void; readonly onMouseEnter?: (event: React.MouseEvent) => void; readonly onMouseLeave?: (event: React.MouseEvent) => void; readonly onPinchZoom?: ( @@ -64,14 +71,14 @@ interface EventCaptureProps { e: React.TouchEvent, ) => void; readonly onPan?: ( - mouseXY: number[], + mouseXY: [number, number], panStartXScale: ScaleContinuousNumeric | ScaleTime, panOrigin: { dx: number; dy: number }, chartsToPan: string[], e: React.MouseEvent, ) => void; readonly onPanEnd?: ( - mouseXY: number[], + mouseXY: [number, number], panStartXScale: ScaleContinuousNumeric | ScaleTime, panOrigin: { dx: number; dy: number }, chartsToPan: string[], @@ -83,7 +90,7 @@ interface EventCaptureProps { interface EventCaptureState { cursorOverrideClass?: string; dragInProgress?: boolean; - dragStartPosition?: number[]; + dragStartPosition?: [number, number]; panInProgress: boolean; panStart?: { panStartXScale: ScaleContinuousNumeric | ScaleTime; @@ -114,7 +121,7 @@ export class EventCapture extends React.Component { + public setCursorClass = (cursorOverrideClass: string | undefined | null) => { if (cursorOverrideClass !== this.state.cursorOverrideClass) { this.setState({ - cursorOverrideClass, + cursorOverrideClass: cursorOverrideClass === null ? undefined : cursorOverrideClass, }); } }; diff --git a/packages/core/src/GenericChartComponent.tsx b/packages/core/src/GenericChartComponent.tsx index fb0108cb..75db6348 100644 --- a/packages/core/src/GenericChartComponent.tsx +++ b/packages/core/src/GenericChartComponent.tsx @@ -1,98 +1,129 @@ -import { GenericComponent } from "./GenericComponent"; +import React, { ForwardedRef, useCallback, useContext } from "react"; +import { GenericComponent, GenericComponentProps, GenericComponentRef, MoreProps } from "./GenericComponent"; import { isDefined } from "./utils"; import { ChartContext } from "./Chart"; const ALWAYS_TRUE_TYPES = ["drag", "dragend"]; -export class GenericChartComponent extends GenericComponent { - public static defaultProps = GenericComponent.defaultProps; - public static contextType = ChartContext; - - public constructor(props: any, context: any) { - super(props, context); - - this.preCanvasDraw = this.preCanvasDraw.bind(this); - this.postCanvasDraw = this.postCanvasDraw.bind(this); - this.shouldTypeProceed = this.shouldTypeProceed.bind(this); - this.preEvaluate = this.preEvaluate.bind(this); - this.updateMoreProps = this.updateMoreProps.bind(this); - } - - public preCanvasDraw(ctx: CanvasRenderingContext2D, moreProps: any) { - super.preCanvasDraw(ctx, moreProps); - - ctx.save(); - const { margin, ratio } = this.context; - const { - chartConfig: { width, height, origin }, - } = moreProps; - - const canvasOriginX = 0.5 * ratio + origin[0] + margin.left; - const canvasOriginY = 0.5 * ratio + origin[1] + margin.top; - - const { clip, edgeClip } = this.props; - - ctx.setTransform(1, 0, 0, 1, 0, 0); - ctx.scale(ratio, ratio); - if (edgeClip) { - ctx.beginPath(); - ctx.rect(-1, canvasOriginY - 10, width + margin.left + margin.right + 1, height + 20); - ctx.clip(); - } - - ctx.translate(canvasOriginX, canvasOriginY); - - if (clip) { - ctx.beginPath(); - ctx.rect(-1, -1, width + 1, height + 1); - ctx.clip(); - } - } - - public postCanvasDraw(ctx: CanvasRenderingContext2D, moreProps: any) { - super.postCanvasDraw(ctx, moreProps); - ctx.restore(); - } - - public updateMoreProps(moreProps: any) { - super.updateMoreProps(moreProps); - const { chartConfig: chartConfigList } = moreProps; - - if (chartConfigList && Array.isArray(chartConfigList)) { - const { chartId } = this.context; - const chartConfig = chartConfigList.find((each) => each.id === chartId); - this.moreProps.chartConfig = chartConfig; - } - if (isDefined(this.moreProps.chartConfig)) { - const { - origin: [ox, oy], - } = this.moreProps.chartConfig; - if (isDefined(moreProps.mouseXY)) { - const { - mouseXY: [x, y], - } = moreProps; - this.moreProps.mouseXY = [x - ox, y - oy]; +const postCanvasDraw = (ctx: CanvasRenderingContext2D) => { + ctx.restore(); +}; + +export const GenericChartComponent = React.memo( + React.forwardRef((props: GenericComponentProps, ref: ForwardedRef) => { + const { clip = true, edgeClip = false } = props; + const context = useContext(ChartContext); + const { chartId, chartConfig } = context; + const getMoreProps = useCallback( + (moreProps: MoreProps) => { + const result: Partial = { + chartConfig, + chartId, + }; + result.chartConfig = moreProps.chartConfigs.find((each) => each.id === chartId); + if (isDefined(moreProps.chartConfig)) { + const { + origin: [ox, oy], + } = moreProps.chartConfig; + if (isDefined(moreProps.mouseXY)) { + const { + mouseXY: [x, y], + } = moreProps; + result.mouseXY = [x - ox, y - oy]; + } + if (isDefined(moreProps.startPos)) { + const { + startPos: [x, y], + } = moreProps; + result.startPos = [x - ox, y - oy]; + } + } + return result; + }, + [chartId], + ); + const preCanvasDraw = useCallback( + (ctx: CanvasRenderingContext2D, moreProps: MoreProps) => { + const chartConfig = moreProps.chartConfigs.find((each) => each.id === chartId); + if (!chartConfig) { + return; + } + ctx.save(); + const { margin, ratio } = context; + const { width, height, origin } = chartConfig; + + const canvasOriginX = 0.5 * ratio + origin[0] + margin.left; + const canvasOriginY = 0.5 * ratio + origin[1] + margin.top; + + ctx.setTransform(1, 0, 0, 1, 0, 0); + ctx.scale(ratio, ratio); + if (edgeClip) { + ctx.beginPath(); + ctx.rect(-1, canvasOriginY - 10, width + margin.left + margin.right + 1, height + 20); + ctx.clip(); + } + + ctx.translate(canvasOriginX, canvasOriginY); + + if (clip) { + ctx.beginPath(); + ctx.rect(-1, -1, width + 1, height + 1); + ctx.clip(); + } + }, + [context.margin, context.ratio, clip, edgeClip, chartConfig], + ); + + const shouldTypeProceed = useCallback((type: string, moreProps: MoreProps) => { + if ((type === "mousemove" || type === "click") && props.disablePan) { + return true; } - if (isDefined(moreProps.startPos)) { - const { - startPos: [x, y], - } = moreProps; - this.moreProps.startPos = [x - ox, y - oy]; + if (ALWAYS_TRUE_TYPES.indexOf(type) === -1 && isDefined(moreProps) && isDefined(moreProps.currentCharts)) { + return moreProps.currentCharts.indexOf(context.chartId) > -1; } - } - } - - public preEvaluate(/* type, moreProps */) { - /// - } - - public shouldTypeProceed(type: string, moreProps: any) { - if ((type === "mousemove" || type === "click") && this.props.disablePan) { return true; - } - if (ALWAYS_TRUE_TYPES.indexOf(type) === -1 && isDefined(moreProps) && isDefined(moreProps.currentCharts)) { - return moreProps.currentCharts.indexOf(this.context.chartId) > -1; - } - return true; - } -} + }, []); + + const updateMoreProps = useCallback( + (newMoreProps: MoreProps | undefined, moreProps: MoreProps) => { + const { chartConfigs: chartConfigList } = newMoreProps || moreProps; + if (chartConfigList && Array.isArray(chartConfigList)) { + const { chartId } = context; + moreProps.chartConfig = chartConfigList.find((each) => each.id === chartId); + } + if (isDefined(moreProps.chartConfig)) { + const { + origin: [ox, oy], + } = moreProps.chartConfig; + if (isDefined(moreProps.mouseXY)) { + const { + mouseXY: [x, y], + } = moreProps; + moreProps.mouseXY = [x - ox, y - oy]; + } + if (isDefined(moreProps.startPos)) { + const { + startPos: [x, y], + } = moreProps; + moreProps.startPos = [x - ox, y - oy]; + } + } + }, + [context.chartId], + ); + + return ( + + ); + }), +); diff --git a/packages/core/src/GenericComponent.tsx b/packages/core/src/GenericComponent.tsx index ba9db2ac..9ce6629f 100644 --- a/packages/core/src/GenericComponent.tsx +++ b/packages/core/src/GenericComponent.tsx @@ -1,8 +1,19 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ -import * as React from "react"; -import { functor, identity } from "./utils"; +import React, { + ForwardedRef, + useCallback, + useContext, + useEffect, + useImperativeHandle, + useMemo, + useRef, + useState, +} from "react"; import { ICanvasContexts } from "./CanvasContainer"; import { ChartCanvasContext } from "./ChartCanvas"; +import { useEvent } from "./useEvent"; +import { ChartConfig } from "./utils/ChartDataUtil"; +import { ChartContext } from "./Chart"; const aliases: Record = { mouseleave: "mousemove", // to draw interactive after mouse exit @@ -18,7 +29,7 @@ const aliases: Record = { zoom: "zoom", }; -interface GenericComponentProps { +export interface GenericComponentProps { readonly svgDraw?: (moreProps: any) => React.ReactNode; readonly canvasDraw?: (ctx: CanvasRenderingContext2D, moreProps: any) => void; readonly canvasToDraw?: (contexts: ICanvasContexts) => CanvasRenderingContext2D | undefined; @@ -46,410 +57,364 @@ interface GenericComponentProps { readonly onHover?: (e: React.MouseEvent, moreProps: any) => void; readonly onUnHover?: (e: React.MouseEvent, moreProps: any) => void; readonly selected?: boolean; + readonly shouldTypeProceed?: (type: string, moreProps: MoreProps) => boolean; + readonly preCanvasDraw?: (ctx: CanvasRenderingContext2D, moreProps: MoreProps) => void; + readonly postCanvasDraw?: (ctx: CanvasRenderingContext2D, moreProps: MoreProps) => void; + readonly updateMoreProps?: (newMoreProps: MoreProps | undefined, moreProps: MoreProps) => void; + readonly getMoreProps?: (moreProps: MoreProps) => Partial; } -interface GenericComponentState { - updateCount: number; +export interface MoreProps { + chartId: string | number; + hovering: boolean; + currentCharts: (string | number)[]; + startPos?: [number, number]; + mouseXY?: [number, number]; + chartConfigs: ChartConfig[]; + chartConfig?: ChartConfig; + fullData: any[]; + plotData: any[]; + xScale: Function; } -export class GenericComponent extends React.Component { - public static defaultProps = { - svgDraw: functor(null), - draw: [], - canvasToDraw: (contexts: ICanvasContexts) => contexts.mouseCoord, - clip: true, - edgeClip: false, - selected: false, - disablePan: false, - enableDragOnHover: false, - }; - - public moreProps: any = {}; - - private dragInProgress = false; - private evaluationInProgress = false; - private iSetTheCursorClass = false; - private readonly subscriberId: number; - - public constructor(props: GenericComponentProps, context: any) { - super(props, context); - this.drawOnCanvas = this.drawOnCanvas.bind(this); - this.getMoreProps = this.getMoreProps.bind(this); - this.draw = this.draw.bind(this); - this.updateMoreProps = this.updateMoreProps.bind(this); - this.evaluateType = this.evaluateType.bind(this); - this.isHover = this.isHover.bind(this); - this.preCanvasDraw = this.preCanvasDraw.bind(this); - this.postCanvasDraw = this.postCanvasDraw.bind(this); - this.getPanConditions = this.getPanConditions.bind(this); - this.shouldTypeProceed = this.shouldTypeProceed.bind(this); - this.preEvaluate = this.preEvaluate.bind(this); - - const { generateSubscriptionId } = context; - - this.subscriberId = generateSubscriptionId(); - - this.state = { - updateCount: 0, - }; - } - - public updateMoreProps(moreProps: any) { - Object.keys(moreProps).forEach((key) => { - this.moreProps[key] = moreProps[key]; - }); - } - - public shouldTypeProceed(type: string, moreProps: any) { - return true; - } - - public preEvaluate(type: string, moreProps: any, e: any) { - /// empty - } +export interface GenericComponentRef { + getMoreProps: () => MoreProps; +} - public listener = (type: string, moreProps: any, state: any, e: any) => { - if (moreProps !== undefined) { - this.updateMoreProps(moreProps); - } - this.evaluationInProgress = true; - this.evaluateType(type, e); - this.evaluationInProgress = false; - }; - - public evaluateType(type: string, e: any) { - // @ts-ignore - const newType = aliases[type] || type; - const proceed = this.props.drawOn.indexOf(newType) > -1; - if (!proceed) { - return; - } +export const GenericComponent = React.memo( + React.forwardRef((props: GenericComponentProps, ref: ForwardedRef) => { + const context = useContext(ChartCanvasContext); + const { chartId } = useContext(ChartContext); + const subscriberId = useMemo(() => context.generateSubscriptionId?.() || 0, []); + const [, setUpdateCount] = useState(0); + const { subscribe, unsubscribe } = context; + const { + clip = true, + edgeClip = false, + canvasToDraw = (contexts: ICanvasContexts) => contexts.mouseCoord, + } = props; + const moreProps = React.useRef({ + chartId: context.chartId, + hovering: false, + currentCharts: [], + chartConfigs: context.chartConfigs, + fullData: context.fullData, + plotData: context.plotData, + xScale: context.xScale, + }); + const dragInProgressRef = useRef(false); + const evaluationInProgressRef = useRef(false); + const iSetTheCursorClassRef = useRef(false); + + const updateMoreProps = useCallback((newMoreProps: MoreProps | undefined, moreProps: MoreProps) => { + Object.assign(moreProps, newMoreProps || {}); + props.updateMoreProps?.(newMoreProps, moreProps); + }, []); + + const getMoreProps = useCallback(() => { + const { chartConfigs, xAccessor, displayXAccessor, width, height, fullData } = context; + + const otherMoreProps = props.getMoreProps?.(moreProps.current); + + return { + xAccessor, + displayXAccessor, + width, + height, + ...moreProps.current, + fullData, + chartConfigs, + ...otherMoreProps, + }; + }, [context, props.getMoreProps]); + + useImperativeHandle( + ref, + () => ({ + getMoreProps, + }), + [getMoreProps], + ); + + const isHover = useCallback( + (e: React.MouseEvent) => { + if (props.isHover === undefined) { + return false; + } - this.preEvaluate(type, this.moreProps, e); + return props.isHover(getMoreProps(), e); + }, + [props.isHover, getMoreProps], + ); + + const preCanvasDraw = useCallback( + (ctx: CanvasRenderingContext2D, moreProps: MoreProps) => { + props.preCanvasDraw?.(ctx, moreProps); + }, + [props.preCanvasDraw], + ); + + const postCanvasDraw = useCallback( + (ctx: CanvasRenderingContext2D, moreProps: MoreProps) => { + props.postCanvasDraw?.(ctx, moreProps); + }, + [props.postCanvasDraw], + ); + + const evaluateType = useEvent((type: string, e: any) => { + const newType = aliases[type] || type; + const proceed = props.drawOn.includes(newType); + if (!proceed) { + return; + } - if (!this.shouldTypeProceed(type, this.moreProps)) { - return; - } + if (props.shouldTypeProceed && !props.shouldTypeProceed(type, moreProps.current)) { + return; + } - switch (type) { - case "zoom": - case "mouseenter": - // DO NOT DRAW FOR THESE EVENTS - break; - case "mouseleave": { - this.moreProps.hovering = false; + switch (type) { + case "zoom": + case "mouseenter": + // DO NOT DRAW FOR THESE EVENTS + break; + case "mouseleave": { + moreProps.current.hovering = false; - if (this.props.onUnHover) { - this.props.onUnHover(e, this.getMoreProps()); - } - break; - } - case "contextmenu": { - if (this.props.onContextMenu) { - this.props.onContextMenu(e, this.getMoreProps()); - } - if (this.moreProps.hovering && this.props.onContextMenuWhenHover) { - this.props.onContextMenuWhenHover(e, this.getMoreProps()); + if (props.onUnHover) { + props.onUnHover(e, getMoreProps()); + } + break; } - break; - } - case "mousedown": { - if (this.props.onMouseDown) { - this.props.onMouseDown(e, this.getMoreProps()); + case "contextmenu": { + if (props.onContextMenu) { + props.onContextMenu(e, getMoreProps()); + } + if (moreProps.current.hovering && props.onContextMenuWhenHover) { + props.onContextMenuWhenHover(e, getMoreProps()); + } + break; } - break; - } - case "click": { - const { onClick, onClickOutside, onClickWhenHover } = this.props; - const moreProps = this.getMoreProps(); - if (moreProps.hovering && onClickWhenHover !== undefined) { - onClickWhenHover(e, moreProps); - } else if (onClickOutside !== undefined) { - onClickOutside(e, moreProps); + case "mousedown": { + if (props.onMouseDown) { + props.onMouseDown(e, getMoreProps()); + } + break; } + case "click": { + const { onClick, onClickOutside, onClickWhenHover } = props; + const moreProps = getMoreProps(); + if (moreProps.hovering && onClickWhenHover !== undefined) { + onClickWhenHover(e, moreProps); + } else if (onClickOutside !== undefined) { + onClickOutside(e, moreProps); + } - if (onClick !== undefined) { - onClick(e, moreProps); - } - break; - } - case "mousemove": { - const prevHover = this.moreProps.hovering; - this.moreProps.hovering = this.isHover(e); - - const { amIOnTop, setCursorClass } = this.context; - - if ( - this.moreProps.hovering && - !this.props.selected && - /* && !prevHover */ - amIOnTop(this.subscriberId) && - this.props.onHover !== undefined - ) { - setCursorClass("react-financial-charts-pointer-cursor"); - this.iSetTheCursorClass = true; - } else if (this.moreProps.hovering && this.props.selected && amIOnTop(this.subscriberId)) { - setCursorClass(this.props.interactiveCursorClass); - this.iSetTheCursorClass = true; - } else if (prevHover && !this.moreProps.hovering && this.iSetTheCursorClass) { - this.iSetTheCursorClass = false; - setCursorClass(null); + if (onClick !== undefined) { + onClick(e, moreProps); + } + break; } - const moreProps = this.getMoreProps(); + case "mousemove": { + const prevHover = moreProps.current.hovering; + moreProps.current.hovering = isHover(e); + + const { amIOnTop, setCursorClass } = context; + + if ( + moreProps.current.hovering && + !props.selected && + /* && !prevHover */ + amIOnTop(subscriberId) && + props.onHover !== undefined + ) { + setCursorClass("react-financial-charts-pointer-cursor"); + iSetTheCursorClassRef.current = true; + } else if (moreProps.current.hovering && props.selected && amIOnTop(subscriberId)) { + setCursorClass(props.interactiveCursorClass); + iSetTheCursorClassRef.current = true; + } else if (prevHover && !moreProps.current.hovering && iSetTheCursorClassRef.current) { + iSetTheCursorClassRef.current = false; + setCursorClass(null); + } + const morePropsSub = getMoreProps(); - if (this.moreProps.hovering && !prevHover) { - if (this.props.onHover) { - this.props.onHover(e, moreProps); + if (moreProps.current.hovering && !prevHover) { + if (props.onHover) { + props.onHover(e, morePropsSub); + } } - } - if (prevHover && !this.moreProps.hovering) { - if (this.props.onUnHover) { - this.props.onUnHover(e, moreProps); + if (prevHover && !moreProps.current.hovering) { + if (props.onUnHover) { + props.onUnHover(e, morePropsSub); + } } - } - if (this.props.onMouseMove) { - this.props.onMouseMove(e, moreProps); + if (props.onMouseMove) { + props.onMouseMove(e, morePropsSub); + } + break; } - break; - } - case "dblclick": { - const moreProps = this.getMoreProps(); + case "dblclick": { + const morePropsSub = getMoreProps(); - if (this.props.onDoubleClick) { - this.props.onDoubleClick(e, moreProps); - } - if (this.moreProps.hovering && this.props.onDoubleClickWhenHover) { - this.props.onDoubleClickWhenHover(e, moreProps); + if (props.onDoubleClick) { + props.onDoubleClick(e, morePropsSub); + } + if (moreProps.current.hovering && props.onDoubleClickWhenHover) { + props.onDoubleClickWhenHover(e, morePropsSub); + } + break; } - break; - } - case "pan": { - this.moreProps.hovering = false; - if (this.props.onPan) { - this.props.onPan(e, this.getMoreProps()); + case "pan": { + moreProps.current.hovering = false; + if (props.onPan) { + props.onPan(e, getMoreProps()); + } + break; } - break; - } - case "panend": { - if (this.props.onPanEnd) { - this.props.onPanEnd(e, this.getMoreProps()); + case "panend": { + if (props.onPanEnd) { + props.onPanEnd(e, getMoreProps()); + } + break; } - break; - } - case "dragstart": { - if (this.getPanConditions().draggable) { - const { amIOnTop } = this.context; - if (amIOnTop(this.subscriberId)) { - this.dragInProgress = true; - if (this.props.onDragStart !== undefined) { - this.props.onDragStart(e, this.getMoreProps()); + case "dragstart": { + if (getPanConditions().draggable) { + const { amIOnTop } = context; + if (amIOnTop(subscriberId)) { + dragInProgressRef.current = true; + if (props.onDragStart !== undefined) { + props.onDragStart(e, getMoreProps()); + } } } + break; } - break; - } - case "drag": { - if (this.dragInProgress && this.props.onDrag) { - this.props.onDrag(e, this.getMoreProps()); + case "drag": { + if (dragInProgressRef.current && props.onDrag) { + props.onDrag(e, getMoreProps()); + } + break; } - break; - } - case "dragend": { - if (this.dragInProgress && this.props.onDragComplete) { - this.props.onDragComplete(e, this.getMoreProps()); + case "dragend": { + if (dragInProgressRef.current && props.onDragComplete) { + props.onDragComplete(e, getMoreProps()); + } + dragInProgressRef.current = false; + break; } - this.dragInProgress = false; - break; - } - case "dragcancel": { - if (this.dragInProgress || this.iSetTheCursorClass) { - const { setCursorClass } = this.context; - setCursorClass(null); + case "dragcancel": { + if (dragInProgressRef.current || iSetTheCursorClassRef.current) { + const { setCursorClass } = context; + setCursorClass(null); + } + break; } - break; } - } - } - - public isHover(e: React.MouseEvent) { - const { isHover } = this.props; - if (isHover === undefined) { - return false; - } + }); - return isHover(this.getMoreProps(), e); - } - - public getPanConditions() { - const draggable = - !!(this.props.selected && this.moreProps.hovering) || - (this.props.enableDragOnHover && this.moreProps.hovering); - - return { - draggable, - panEnabled: !this.props.disablePan, - }; - } - - public draw({ trigger, force = false }: { force: boolean; trigger: string }) { - const type = aliases[trigger] || trigger; - const proceed = this.props.drawOn.indexOf(type) > -1; - - if (proceed || this.props.selected /* this is to draw as soon as you select */ || force) { - const { canvasDraw } = this.props; - if (canvasDraw === undefined) { - const { updateCount } = this.state; - this.setState({ - updateCount: updateCount + 1, - }); - } else { - this.drawOnCanvas(); + const listener = useEvent((type: string, newMoreProps: MoreProps | undefined, state: any, e: any) => { + if (newMoreProps) { + updateMoreProps(newMoreProps, moreProps.current); } - } - } - - public UNSAFE_componentWillMount() { - const { subscribe, chartId } = this.context; - const { clip, edgeClip } = this.props; - - subscribe(this.subscriberId, { - chartId, - clip, - edgeClip, - listener: this.listener, - draw: this.draw, - getPanConditions: this.getPanConditions, + evaluationInProgressRef.current = true; + evaluateType(type, e); + evaluationInProgressRef.current = false; }); - this.UNSAFE_componentWillReceiveProps(this.props, this.context); - } + const drawOnCanvas = useCallback(() => { + const { canvasDraw } = props; + if (canvasDraw === undefined || canvasToDraw === undefined) { + return; + } - public componentWillUnmount() { - const { unsubscribe } = this.context; - unsubscribe(this.subscriberId); - if (this.iSetTheCursorClass) { - const { setCursorClass } = this.context; - setCursorClass(null); - } - } + const moreProps = getMoreProps(); - public componentDidMount() { - this.componentDidUpdate(this.props); - } + const contexts = context.getCanvasContexts?.(); - public componentDidUpdate(prevProps: GenericComponentProps) { - const { canvasDraw, selected, interactiveCursorClass } = this.props; + if (contexts === undefined) { + return; + } - if (prevProps.selected !== selected) { - const { setCursorClass } = this.context; - if (selected && this.moreProps.hovering) { - this.iSetTheCursorClass = true; - setCursorClass(interactiveCursorClass); + const ctx = canvasToDraw(contexts); + if (ctx !== undefined) { + preCanvasDraw(ctx, moreProps); + canvasDraw(ctx, moreProps); + postCanvasDraw(ctx, moreProps); + } + }, [canvasToDraw, props.canvasDraw, context.getCanvasContexts, preCanvasDraw, postCanvasDraw, getMoreProps]); + + const draw = useEvent(({ trigger, force = false }: { force: boolean; trigger: string }) => { + const type = aliases[trigger] || trigger; + const proceed = props.drawOn.indexOf(type) > -1; + + if (proceed || props.selected /* this is to draw as soon as you select */ || force) { + const { canvasDraw } = props; + if (canvasDraw === undefined) { + setUpdateCount((u) => u + 1); + } else { + drawOnCanvas(); + } + } + }); + const getPanConditions = useEvent(() => { + const draggable = + (props.selected && moreProps.current.hovering) || + (props.enableDragOnHover && moreProps.current.hovering); + + return { + draggable: !!draggable, + panEnabled: !props.disablePan, + }; + }); + + useEffect(() => { + const { setCursorClass } = context; + if (props.selected && moreProps.current.hovering) { + iSetTheCursorClassRef.current = true; + setCursorClass(props.interactiveCursorClass); } else { - this.iSetTheCursorClass = false; + iSetTheCursorClassRef.current = false; setCursorClass(null); } - } - if (canvasDraw !== undefined && !this.evaluationInProgress) { - this.updateMoreProps(this.moreProps); - this.drawOnCanvas(); - } - } - - public UNSAFE_componentWillReceiveProps(nextProps: GenericComponentProps, nextContext: any) { - const { xScale, plotData, chartConfig, getMutableState } = nextContext; - - this.moreProps = { - ...this.moreProps, - ...getMutableState(), - /* - ^ this is so - mouseXY, currentCharts, currentItem are available to - newly created components like MouseHoverText which - is created right after a new interactive object is drawn - */ - xScale, - plotData, - chartConfig, - }; - } - - public getMoreProps() { - const { - xScale, - plotData, - chartConfig, - morePropsDecorator, - xAccessor, - displayXAccessor, - width, - height, - } = this.context; - - const { chartId, fullData } = this.context; - - const moreProps = { - xScale, - plotData, - chartConfig, - xAccessor, - displayXAccessor, - width, - height, - chartId, - fullData, - ...this.moreProps, - }; - - return (morePropsDecorator || identity)(moreProps); - } - - public preCanvasDraw(ctx: CanvasRenderingContext2D, moreProps: any) { - // do nothing - } - - public postCanvasDraw(ctx: CanvasRenderingContext2D, moreProps: any) { - // empty - } - - public drawOnCanvas() { - const { canvasDraw, canvasToDraw } = this.props; - if (canvasDraw === undefined || canvasToDraw === undefined) { - return; - } + }, [props.selected]); - const { getCanvasContexts } = this.context; - - const moreProps = this.getMoreProps(); - - const contexts = getCanvasContexts(); + useEffect(() => { + if (props.canvasDraw !== undefined && !evaluationInProgressRef.current) { + updateMoreProps(undefined, moreProps.current); + drawOnCanvas(); + } + }); - const ctx = canvasToDraw(contexts); - if (ctx !== undefined) { - this.preCanvasDraw(ctx, moreProps); - canvasDraw(ctx, moreProps); - this.postCanvasDraw(ctx, moreProps); - } - } + useEffect(() => { + subscribe(subscriberId, { + chartId, + clip, + edgeClip, + listener, + draw, + getPanConditions, + }); + return () => { + unsubscribe(subscriberId); + if (iSetTheCursorClassRef.current) { + context.setCursorClass(null); + } + }; + }, [chartId, subscriberId, edgeClip, clip]); - public render() { - const { canvasDraw, clip, svgDraw } = this.props; + const { canvasDraw, svgDraw } = props; if (canvasDraw !== undefined || svgDraw === undefined) { return null; } - const { chartId } = this.context; - const suffix = chartId !== undefined ? "-" + chartId : ""; const style = clip ? { clipPath: `url(#chart-area-clip${suffix})` } : undefined; - return {svgDraw(this.getMoreProps())}; - } -} - -GenericComponent.contextType = ChartCanvasContext; + return {svgDraw(getMoreProps())}; + }), +); export const getAxisCanvas = (contexts: ICanvasContexts) => { return contexts.axes; diff --git a/packages/core/src/useEvent.ts b/packages/core/src/useEvent.ts new file mode 100644 index 00000000..23549cdf --- /dev/null +++ b/packages/core/src/useEvent.ts @@ -0,0 +1,14 @@ +import { useCallback, useLayoutEffect, useRef } from "react"; +// Based on https://github.com/reactjs/rfcs/blob/useevent/text/0000-useevent.md#internal-implementation +export function useEvent any>(handler: F): (...args: Parameters) => ReturnType { + const handlerRef = useRef(); + + useLayoutEffect(() => { + handlerRef.current = handler; + }); + + return useCallback((...args: Parameters) => { + const fn = handlerRef.current!; + return fn(...args); + }, []); +} diff --git a/packages/core/src/utils/ChartDataUtil.ts b/packages/core/src/utils/ChartDataUtil.ts index 46a5a46e..fca2a004 100644 --- a/packages/core/src/utils/ChartDataUtil.ts +++ b/packages/core/src/utils/ChartDataUtil.ts @@ -150,9 +150,9 @@ export function getNewChartConfig(innerDimension: any, children: any, existingCh }).filter((each: any) => each !== undefined); } -export function getCurrentCharts(chartConfig: any, mouseXY: number[]) { +export function getCurrentCharts(chartConfig: ChartConfig[], mouseXY: number[]) { const currentCharts = chartConfig - .filter((eachConfig: any) => { + .filter((eachConfig) => { const top = eachConfig.origin[1]; const bottom = top + eachConfig.height; return mouseXY[1] > top && mouseXY[1] < bottom; diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts index 7fda4ab2..627823d5 100644 --- a/packages/core/src/utils/index.ts +++ b/packages/core/src/utils/index.ts @@ -1,3 +1,4 @@ +import React from "react"; export { default as zipper } from "./zipper"; export { default as slidingWindow } from "./slidingWindow"; export * from "./closestItem"; @@ -100,7 +101,7 @@ export function last(array: any[], accessor?: any) { return length ? array[length - 1] : undefined; } -export const isDefined = (d: T) => { +export const isDefined = (d: T): d is NonNullable => { return d !== null && d !== undefined; }; diff --git a/packages/series/src/AreaSeries.tsx b/packages/series/src/AreaSeries.tsx index b635d23f..ad424831 100644 --- a/packages/series/src/AreaSeries.tsx +++ b/packages/series/src/AreaSeries.tsx @@ -14,7 +14,7 @@ export interface AreaSeriesProps { | ((yScale: ScaleContinuousNumeric, d: [number, number], moreProps: any) => number); readonly canvasClip?: (context: CanvasRenderingContext2D, moreProps: any) => void; /** - * Wether to connect the area between undefined data points. + * Whether to connect the area between undefined data points. */ readonly connectNulls?: boolean; /** diff --git a/packages/tooltip/src/MovingAverageTooltip.tsx b/packages/tooltip/src/MovingAverageTooltip.tsx index 528991d9..a77a4426 100644 --- a/packages/tooltip/src/MovingAverageTooltip.tsx +++ b/packages/tooltip/src/MovingAverageTooltip.tsx @@ -1,4 +1,4 @@ -import { functor, GenericChartComponent, last } from "@react-financial-charts/core"; +import { functor, GenericChartComponent, last, MoreProps } from "@react-financial-charts/core"; import { format } from "d3-format"; import * as React from "react"; import { ToolTipText } from "./ToolTipText"; @@ -96,13 +96,8 @@ export class MovingAverageTooltip extends React.Component; } - private readonly renderSVG = (moreProps: any) => { - const { - chartId, - chartConfig, - chartConfig: { height }, - fullData, - } = moreProps; + private readonly renderSVG = (moreProps: MoreProps) => { + const { chartId, chartConfig, chartConfig: { height = 0 } = {}, fullData } = moreProps; const { className, @@ -122,7 +117,7 @@ export class MovingAverageTooltip extends React.Component