From 119c48c8df3b5ee94f36bfdaf86d05bb8e03fa84 Mon Sep 17 00:00:00 2001 From: Chris Williams Date: Tue, 24 Nov 2020 20:05:59 -0800 Subject: [PATCH 01/24] new(xychart): add event source constants --- packages/visx-xychart/src/constants.ts | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 packages/visx-xychart/src/constants.ts diff --git a/packages/visx-xychart/src/constants.ts b/packages/visx-xychart/src/constants.ts new file mode 100644 index 000000000..1b3441385 --- /dev/null +++ b/packages/visx-xychart/src/constants.ts @@ -0,0 +1,8 @@ +// event sources +export const XYCHART_EVENT_SOURCE = 'XYCHART_EVENT_SOURCE'; +export const AREASERIES_EVENT_SOURCE = 'AREASERIES_EVENT_SOURCE'; +export const BARGROUP_EVENT_SOURCE = 'BARGROUP_EVENT_SOURCE'; +export const BARSERIES_EVENT_SOURCE = 'BARSERIES_EVENT_SOURCE'; +export const BARSTACK_EVENT_SOURCE = 'BARSTACK_EVENT_SOURCE'; +export const GLYPHSERIES_EVENT_SOURCE = 'GLYPHSERIES_EVENT_SOURCE'; +export const LINESERIES_EVENT_SOURCE = 'LINESERIES_EVENT_SOURCE'; From 754b523090f553aac86ba25c02e84498dbefbd4b Mon Sep 17 00:00:00 2001 From: Chris Williams Date: Tue, 24 Nov 2020 20:06:14 -0800 Subject: [PATCH 02/24] new(xychart): add hooks/usePointerEventEmitters --- .../src/hooks/usePointerEventEmitters.ts | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 packages/visx-xychart/src/hooks/usePointerEventEmitters.ts diff --git a/packages/visx-xychart/src/hooks/usePointerEventEmitters.ts b/packages/visx-xychart/src/hooks/usePointerEventEmitters.ts new file mode 100644 index 000000000..33a0b131f --- /dev/null +++ b/packages/visx-xychart/src/hooks/usePointerEventEmitters.ts @@ -0,0 +1,42 @@ +import { useCallback } from 'react'; +import useEventEmitter from './useEventEmitter'; + +type PointerEventEmitterParams = { + /** Source of the events, e.g., the component name. */ + source: string; + onPointerMove?: boolean; + onPointerOut?: boolean; + onPointerUp?: boolean; +}; + +/** + * A hook that simplifies creation of handlers for emitting + * pointermove, pointerout, and pointerup events to EventEmitterContext. + */ +export default function usePointerEventEmitters({ + source, + onPointerOut = true, + onPointerMove = true, + onPointerUp = true, +}: PointerEventEmitterParams) { + const emit = useEventEmitter(); + + const emitPointerMove = useCallback( + (event: React.PointerEvent) => emit?.('pointermove', event, source), + [emit], + ); + const emitPointerOut = useCallback( + (event: React.PointerEvent) => emit?.('pointerout', event, source), + [emit], + ); + const emitPointerUp = useCallback( + (event: React.PointerEvent) => emit?.('pointerup', event, source), + [emit], + ); + + return { + onPointerMove: onPointerMove ? emitPointerMove : undefined, + onPointerOut: onPointerOut ? emitPointerOut : undefined, + onPointerUp: onPointerUp ? emitPointerUp : undefined, + }; +} From 9b8ebfd506f614ea9c10f89159b891fa48b17691 Mon Sep 17 00:00:00 2001 From: Chris Williams Date: Tue, 24 Nov 2020 20:06:31 -0800 Subject: [PATCH 03/24] new(xychart): add hooks/usePointerEventHandlers --- .../src/hooks/usePointerEventHandlers.ts | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 packages/visx-xychart/src/hooks/usePointerEventHandlers.ts diff --git a/packages/visx-xychart/src/hooks/usePointerEventHandlers.ts b/packages/visx-xychart/src/hooks/usePointerEventHandlers.ts new file mode 100644 index 000000000..3342968d5 --- /dev/null +++ b/packages/visx-xychart/src/hooks/usePointerEventHandlers.ts @@ -0,0 +1,117 @@ +import { PointerEvent, useCallback, useContext } from 'react'; +import DataContext from '../context/DataContext'; +import { PointerEventParams } from '../types'; +import findNearestDatumX from '../utils/findNearestDatumX'; +import findNearestDatumY from '../utils/findNearestDatumY'; +import { HandlerParams } from './useEventEmitter'; + +export const POINTER_EVENTS_ALL = '__all'; +export const POINTER_EVENTS_NEAREST = '__nearest'; + +type PointerEventHandlerParams = { + /** Controls whether callbacks are invoked for a given registry dataKey, the nearest dataKey, or all datakeys. */ + dataKey: string | string[] | typeof POINTER_EVENTS_ALL | typeof POINTER_EVENTS_NEAREST; + /** Callback invoked onPointerMove for one or more series based on dataKey. */ + onPointerMove?: (params: PointerEventParams) => void; + /** Callback invoked onPointerOut for one or more series based on dataKey. */ + onPointerOut?: (event: PointerEvent) => void; + /** Callback invoked onPointerUp for one or more series based on dataKey. */ + onPointerUp?: (params: PointerEventParams) => void; +}; + +/** + * Hook that returns PointerEvent handlers that invoke the passed pointer + * handlers with the nearest datum to the event for the passed dataKey. + */ +export default function usePointerEventHandlers({ + dataKey, + onPointerMove, + onPointerOut, + onPointerUp, +}: PointerEventHandlerParams) { + const { width, height, horizontal, dataRegistry, xScale, yScale } = useContext(DataContext); + + const handlePointerMoveOrUp = useCallback( + (params?: HandlerParams) => { + const { svgPoint, event } = params || {}; + + const pointerParamsByKey: { [dataKey: string]: PointerEventParams } = {}; + + // nearest Datum across all dataKeys, if relevant + let nearestDatumPointerParams: PointerEventParams | null = null; + let nearestDatumDistance = Infinity; + + if (params && event && svgPoint && width && height && xScale && yScale) { + const considerAllKeys = + dataKey === POINTER_EVENTS_NEAREST || dataKey === POINTER_EVENTS_ALL; + + // find nearestDatum for relevant dataKey(s) + (considerAllKeys + ? dataRegistry?.keys() + : Array.isArray(dataKey) + ? dataKey + : [dataKey] + )?.forEach(key => { + const entry = dataRegistry?.get(key); + if (entry) { + const nearestDatum = (horizontal ? findNearestDatumY : findNearestDatumX)({ + data: entry.data, + height, + point: svgPoint, + width, + xAccessor: entry.xAccessor, + xScale, + yAccessor: entry.yAccessor, + yScale, + }); + if (nearestDatum) { + pointerParamsByKey[key] = { key, svgPoint, event, ...nearestDatum }; + + // compute nearest Datum if not emitting events for all keys\ + if (dataKey === POINTER_EVENTS_NEAREST) { + const distance = Math.sqrt( + (nearestDatum.distanceX ?? Infinity ** 2) + + (nearestDatum.distanceY ?? Infinity ** 2), + ); + nearestDatumPointerParams = + distance < nearestDatumDistance + ? pointerParamsByKey[key] + : nearestDatumPointerParams; + nearestDatumDistance = Math.min(nearestDatumDistance, distance); + } + } + } + }); + + const pointerParams: (null | PointerEventParams)[] = + dataKey === POINTER_EVENTS_NEAREST + ? [nearestDatumPointerParams] + : dataKey === POINTER_EVENTS_ALL || Array.isArray(dataKey) + ? Object.values(pointerParamsByKey) + : [pointerParamsByKey[dataKey]]; + + pointerParams.forEach(p => { + if (p?.event.type === 'pointerup' && onPointerUp) { + onPointerUp(p); + } else if (p?.event.type === 'pointermove') { + if (onPointerMove) onPointerMove(p); + } + }); + } + }, + [dataKey, xScale, yScale, width, height, horizontal, onPointerMove, onPointerOut, onPointerUp], + ); + + const handlePointerOut = useCallback( + (params?: HandlerParams) => { + if (params && onPointerOut) onPointerOut(params.event); + }, + [onPointerOut], + ); + + return { + onPointerMove: onPointerMove ? handlePointerMoveOrUp : undefined, + onPointerOut: onPointerOut ? handlePointerOut : undefined, + onPointerUp: onPointerUp ? handlePointerMoveOrUp : undefined, + }; +} From be548890cbb3381841cf695cbada4c84832fc5dc Mon Sep 17 00:00:00 2001 From: Chris Williams Date: Tue, 24 Nov 2020 20:07:27 -0800 Subject: [PATCH 04/24] new(xychart/useEventEmitter): update to pointer events, add prop annotations, add source filtering --- .../visx-xychart/src/hooks/useEventEmitter.ts | 36 ++++++++++++++----- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/packages/visx-xychart/src/hooks/useEventEmitter.ts b/packages/visx-xychart/src/hooks/useEventEmitter.ts index 20fb91b5f..15fd917b6 100644 --- a/packages/visx-xychart/src/hooks/useEventEmitter.ts +++ b/packages/visx-xychart/src/hooks/useEventEmitter.ts @@ -2,10 +2,15 @@ import { useCallback, useContext, useEffect } from 'react'; import { localPoint } from '@visx/event'; import EventEmitterContext from '../context/EventEmitterContext'; -export type EventType = 'mousemove' | 'mouseout' | 'touchmove' | 'touchend' | 'click'; +export type EventType = 'pointermove' | 'pointerout' | 'pointerup'; + export type HandlerParams = { - event: React.MouseEvent | React.TouchEvent; + /** The react PointerEvent. */ + event: React.PointerEvent; + /** Position of the PointerEvent in svg coordinates. */ svgPoint: ReturnType; + /** The source of the event. This can be anything, but for this package is the name of the component which emitted the event. */ + source?: string; }; export type Handler = (params?: HandlerParams) => void; @@ -13,23 +18,36 @@ export type Handler = (params?: HandlerParams) => void; * Hook for optionally subscribing to a specified EventType, * and returns emitter for emitting events. */ -export default function useEventEmitter(eventType?: EventType, handler?: Handler) { +export default function useEventEmitter( + /** Type of event to subscribe to. */ + eventType?: EventType, + /** Handler invoked on emission of EventType event. */ + handler?: Handler, + /** Optional valid sources for EventType subscription. */ + sources?: string[], +) { const emitter = useContext(EventEmitterContext); - /** wrap emitter.emit so we can enforce stricter type signature */ + // wrap emitter.emit so we can enforce stricter type signature and filter events by source const emit = useCallback( - (type: EventType, event: HandlerParams['event']) => - emitter?.emit(type, { event, svgPoint: localPoint(event) }), + (type: EventType, event: HandlerParams['event'], source?: string) => { + emitter?.emit(type, { event, svgPoint: localPoint(event), source }); + }, [emitter], ); useEffect(() => { if (emitter && eventType && handler) { - emitter.on(eventType, handler); - return () => emitter?.off(eventType, handler); + const handlerWithSourceFilter: Handler = sources + ? (params?: HandlerParams) => { + if (!params?.source || sources.includes(params.source)) handler(params); + } + : handler; + emitter.on(eventType, handlerWithSourceFilter); + return () => emitter?.off(eventType, handlerWithSourceFilter); } return undefined; - }, [emitter, eventType, handler]); + }, [emitter, eventType, handler, sources]); return emitter ? emit : null; } From 12509aada48594f1b4f75fab666f671dbe91a4c2 Mon Sep 17 00:00:00 2001 From: Chris Williams Date: Tue, 24 Nov 2020 20:08:04 -0800 Subject: [PATCH 05/24] internal(xychart/findNearestDatum): factor out return type to types --- packages/visx-xychart/src/types/event.ts | 8 ++++++++ packages/visx-xychart/src/utils/findNearestDatumX.ts | 9 ++------- packages/visx-xychart/src/utils/findNearestDatumXY.ts | 4 ++-- packages/visx-xychart/src/utils/findNearestDatumY.ts | 9 ++------- 4 files changed, 14 insertions(+), 16 deletions(-) diff --git a/packages/visx-xychart/src/types/event.ts b/packages/visx-xychart/src/types/event.ts index 73a907b47..8a129634e 100644 --- a/packages/visx-xychart/src/types/event.ts +++ b/packages/visx-xychart/src/types/event.ts @@ -19,3 +19,11 @@ export type NearestDatumArgs< xScale: XScale; yScale: YScale; }; + +/** Return type for nearestDatum* functions. */ +export type NearestDatumReturnType = { + datum: Datum; + index: number; + distanceX: number; + distanceY: number; +} | null; diff --git a/packages/visx-xychart/src/utils/findNearestDatumX.ts b/packages/visx-xychart/src/utils/findNearestDatumX.ts index 29a35261f..830fad476 100644 --- a/packages/visx-xychart/src/utils/findNearestDatumX.ts +++ b/packages/visx-xychart/src/utils/findNearestDatumX.ts @@ -1,6 +1,6 @@ import { AxisScale } from '@visx/axis'; import findNearestDatumSingleDimension from './findNearestDatumSingleDimension'; -import { NearestDatumArgs } from '../types'; +import { NearestDatumArgs, NearestDatumReturnType } from '../types'; export default function findNearestDatumX< XScale extends AxisScale, @@ -13,12 +13,7 @@ export default function findNearestDatumX< yAccessor, point, data, -}: NearestDatumArgs): { - datum: Datum; - index: number; - distanceX: number; - distanceY: number; -} | null { +}: NearestDatumArgs): NearestDatumReturnType { if (!point) return null; const nearestDatum = findNearestDatumSingleDimension({ diff --git a/packages/visx-xychart/src/utils/findNearestDatumXY.ts b/packages/visx-xychart/src/utils/findNearestDatumXY.ts index d56931c2d..388cf2fd2 100644 --- a/packages/visx-xychart/src/utils/findNearestDatumXY.ts +++ b/packages/visx-xychart/src/utils/findNearestDatumXY.ts @@ -1,6 +1,6 @@ import { AxisScale } from '@visx/axis'; import { voronoi } from '@visx/voronoi'; -import { NearestDatumArgs } from '../types'; +import { NearestDatumArgs, NearestDatumReturnType } from '../types'; /* finds the datum nearest to svgMouseX/Y using a voronoi */ export default function findNearestDatumXY< @@ -16,7 +16,7 @@ export default function findNearestDatumXY< yAccessor, point, data, -}: NearestDatumArgs) { +}: NearestDatumArgs): NearestDatumReturnType { if (!point) return null; const scaledX = (d: Datum) => Number(xScale(xAccessor(d))); diff --git a/packages/visx-xychart/src/utils/findNearestDatumY.ts b/packages/visx-xychart/src/utils/findNearestDatumY.ts index f516b9ec2..02886fd3a 100644 --- a/packages/visx-xychart/src/utils/findNearestDatumY.ts +++ b/packages/visx-xychart/src/utils/findNearestDatumY.ts @@ -1,6 +1,6 @@ import { AxisScale } from '@visx/axis'; import findNearestDatumSingleDimension from './findNearestDatumSingleDimension'; -import { NearestDatumArgs } from '../types'; +import { NearestDatumArgs, NearestDatumReturnType } from '../types'; export default function findNearestDatumY< XScale extends AxisScale, @@ -13,12 +13,7 @@ export default function findNearestDatumY< xAccessor, point, data, -}: NearestDatumArgs): { - datum: Datum; - index: number; - distanceX: number; - distanceY: number; -} | null { +}: NearestDatumArgs): NearestDatumReturnType { if (!point) return null; const nearestDatum = findNearestDatumSingleDimension({ From 16e247fae936d8739589fc7585a2398695c135e7 Mon Sep 17 00:00:00 2001 From: Chris Williams Date: Tue, 24 Nov 2020 20:09:08 -0800 Subject: [PATCH 06/24] types(xychart): add pointer handlers to series types --- packages/visx-xychart/src/types/series.ts | 87 +++++++++++++++++++++- packages/visx-xychart/src/types/tooltip.ts | 19 +---- 2 files changed, 88 insertions(+), 18 deletions(-) diff --git a/packages/visx-xychart/src/types/series.ts b/packages/visx-xychart/src/types/series.ts index 343df53ed..6760a578c 100644 --- a/packages/visx-xychart/src/types/series.ts +++ b/packages/visx-xychart/src/types/series.ts @@ -1,7 +1,26 @@ +import { PointerEvent } from 'react'; import { AxisScale } from '@visx/axis'; import { ScaleInput } from '@visx/scale'; import { Series, SeriesPoint } from 'd3-shape'; +/** Call signature of PointerEvent callback. */ +export type PointerEventParams = { + /** Series key that datum belongs to. */ + key: string; + /** Index of datum in series data array. */ + index: number; + /** Datum. */ + datum: Datum; + /** Optional distance of datum x value to event x value. Used to determine closest datum. */ + distanceX?: number; + /** Optional distance of datum y value to event y value. Used to determine closest datum. */ + distanceY?: number; + /** Coordinates of the event in svg space. */ + svgPoint?: { x: number; y: number }; + /** The PointerEvent. */ + event: PointerEvent; +}; + export type SeriesProps< XScale extends AxisScale, YScale extends AxisScale, @@ -15,15 +34,63 @@ export type SeriesProps< xAccessor: (d: Datum) => ScaleInput; /** Given a Datum, returns the y-scale value. */ yAccessor: (d: Datum) => ScaleInput; + /** + * Callback invoked for onPointerMove events for the nearest Datum to the PointerEvent. + * By default XYChart will capture and emit PointerEvents, invoking this function for + * any Series with a defined handler. Alternatively you may set + * and Series will emit their own events. + */ + onPointerMove?: ({ + datum, + distanceX, + distanceY, + event, + index, + key, + svgPoint, + }: PointerEventParams) => void; + /** + * Callback invoked for onPointerOut events. By default XYChart will capture and emit + * PointerEvents, invoking this function for any Series with a defined handler. + * Alternatively you may set and Series will emit + * their own events. + */ + onPointerOut?: ( + /** The PointerEvent. */ + event: React.PointerEvent, + ) => void; + /** + * Callback invoked for onPointerUp events for the nearest Datum to the PointerEvent. + * By default XYChart will capture and emit PointerEvents, invoking this function for + * any Series with a defined handler. Alternatively you may set + * and Series will emit their own events. + */ + onPointerUp?: ({ + datum, + distanceX, + distanceY, + event, + index, + key, + svgPoint, + }: PointerEventParams) => void; + /** Whether the Series reacts to PointerEvents. */ + pointerEvents?: boolean; }; /** Bar shape. */ export type Bar = { + /** Unique key for Bar (not dataKey). */ key: string; + /** X coordinate of Bar. */ x: number; + /** Y coordinate of Bar. */ y: number; + /** Width coordinate of Bar. */ width: number; + /** Height coordinate of Bar. */ height: number; + /** Fill color of Bar */ fill?: string; }; @@ -50,7 +117,6 @@ export type CombinedStackData | ScaleInput; positiveSum: number; negativeSum: number }; /** Glyphs */ - export type GlyphsProps< XScale extends AxisScale, YScale extends AxisScale, @@ -60,14 +126,33 @@ export type GlyphsProps< yScale: YScale; horizontal?: boolean; glyphs: GlyphProps[]; + /** Callback to invoke for onPointerMove. */ + onPointerMove?: (event: PointerEvent) => void; + /** Callback to invoke for onPointerOut. */ + onPointerOut?: (event: PointerEvent) => void; + /** Callback to invoke for onPointerUp. */ + onPointerUp?: (event: PointerEvent) => void; }; export type GlyphProps = { + /** Unique key for Glyph (not dataKey). */ key: string; + /** Datum for Glyph. */ datum: Datum; + /** Index of Datum in data array. */ index: number; + /** X coordinate of Glyph. */ x: number; + /** Y coordinate of Glyph. */ y: number; + /** Size of Glyph. */ size: number; + /** Color of Glyph. */ color: string; + /** Callback to invoke for onPointerMove. */ + onPointerMove?: (event: PointerEvent) => void; + /** Callback to invoke for onPointerOut. */ + onPointerOut?: (event: PointerEvent) => void; + /** Callback to invoke for onPointerUp. */ + onPointerUp?: (event: PointerEvent) => void; }; diff --git a/packages/visx-xychart/src/types/tooltip.ts b/packages/visx-xychart/src/types/tooltip.ts index a1ccc1c73..1b96d5b85 100644 --- a/packages/visx-xychart/src/types/tooltip.ts +++ b/packages/visx-xychart/src/types/tooltip.ts @@ -1,4 +1,5 @@ import { UseTooltipParams } from '@visx/tooltip/lib/hooks/useTooltip'; +import { PointerEventParams } from './series'; export type TooltipDatum = { /** Series key that datum belongs to. */ @@ -9,22 +10,6 @@ export type TooltipDatum = { datum: Datum; }; -/** Call signature of `TooltipContext.showTooltip` */ -export type ShowTooltipParams = { - /** Series key that datum belongs to. */ - key: string; - /** Index of datum in series data array. */ - index: number; - /** Datum. */ - datum: Datum; - /** Optional distance of datum x value to event x value. Used to determine closest datum. */ - distanceX?: number; - /** Optional distance of datum y value to event y value. Used to determine closest datum. */ - distanceY?: number; - /** Coordinates of the event in svg space. */ - svgPoint?: { x: number; y: number }; -}; - export type TooltipData = { /** Nearest Datum to event across all Series. */ nearestDatum?: TooltipDatum & { distance: number }; @@ -35,5 +20,5 @@ export type TooltipData = { }; export type TooltipContextType = UseTooltipParams> & { - showTooltip: (params: ShowTooltipParams) => void; + showTooltip: (params: PointerEventParams) => void; }; From c4c55ce825399fcdb55f3656604bba308e352339 Mon Sep 17 00:00:00 2001 From: Chris Williams Date: Tue, 24 Nov 2020 20:10:32 -0800 Subject: [PATCH 07/24] new(xychart/XYChart): add pointer handlers, refactor to usePointerEventEmitters/Handlers --- .../visx-xychart/src/components/XYChart.tsx | 73 +++++++++++++++---- 1 file changed, 58 insertions(+), 15 deletions(-) diff --git a/packages/visx-xychart/src/components/XYChart.tsx b/packages/visx-xychart/src/components/XYChart.tsx index 59dbec0d6..d2920d9fd 100644 --- a/packages/visx-xychart/src/components/XYChart.tsx +++ b/packages/visx-xychart/src/components/XYChart.tsx @@ -5,22 +5,29 @@ import { AxisScaleOutput } from '@visx/axis'; import { ScaleConfig } from '@visx/scale'; import DataContext from '../context/DataContext'; -import { Margin } from '../types'; +import { Margin, PointerEventParams } from '../types'; import useEventEmitter from '../hooks/useEventEmitter'; import EventEmitterProvider from '../providers/EventEmitterProvider'; import TooltipContext from '../context/TooltipContext'; import TooltipProvider from '../providers/TooltipProvider'; import DataProvider, { DataProviderProps } from '../providers/DataProvider'; +import usePointerEventEmitters from '../hooks/usePointerEventEmitters'; +import { XYCHART_EVENT_SOURCE } from '../constants'; +import usePointerEventHandlers, { + POINTER_EVENTS_ALL, + POINTER_EVENTS_NEAREST, +} from '../hooks/usePointerEventHandlers'; const DEFAULT_MARGIN = { top: 50, right: 50, bottom: 50, left: 50 }; export type XYChartProps< XScaleConfig extends ScaleConfig, - YScaleConfig extends ScaleConfig + YScaleConfig extends ScaleConfig, + Datum extends object > = { /** aria-label for the chart svg element. */ accessibilityLabel?: string; - /** Whether to capture and dispatch pointer events. */ + /** Whether to capture and dispatch pointer events to EventEmitter context (which e.g., Series subscribe to). */ captureEvents?: boolean; /** Total width of the desired chart svg, including margin. */ width?: number; @@ -36,18 +43,52 @@ export type XYChartProps< xScale?: DataProviderProps['xScale']; /** If DataContext is not available, XYChart will wrap itself in a DataProvider and set this as the yScale config. */ yScale?: DataProviderProps['yScale']; + /** Callback invoked for onPointerMove events for the nearest Datum to the PointerEvent _for each Series with pointerEvents={true}_. */ + onPointerMove?: ({ + datum, + distanceX, + distanceY, + event, + index, + key, + svgPoint, + }: PointerEventParams) => void; + /** Callback invoked for onPointerOut events for the nearest Datum to the PointerEvent _for each Series with pointerEvents={true}_. */ + onPointerOut?: ( + /** The PointerEvent. */ + event: React.PointerEvent, + ) => void; + /** Callback invoked for onPointerUp events for the nearest Datum to the PointerEvent _for each Series with pointerEvents={true}_. */ + onPointerUp?: ({ + datum, + distanceX, + distanceY, + event, + index, + key, + svgPoint, + }: PointerEventParams) => void; + /** Whether to invoke PointerEvent handlers for all dataKeys, or the nearest dataKey. */ + pointerEvents?: 'all' | 'nearest'; }; +const eventSourceSubscriptions = [XYCHART_EVENT_SOURCE]; + export default function XYChart< XScaleConfig extends ScaleConfig, - YScaleConfig extends ScaleConfig ->(props: XYChartProps) { + YScaleConfig extends ScaleConfig, + Datum extends object +>(props: XYChartProps) { const { accessibilityLabel = 'XYChart', captureEvents = true, children, height, margin = DEFAULT_MARGIN, + onPointerMove, + onPointerOut, + onPointerUp, + pointerEvents = 'nearest', theme, width, xScale, @@ -64,13 +105,6 @@ export default function XYChart< } }, [setDimensions, width, height, margin]); - const handlePointerMove = useCallback((event: React.PointerEvent) => emit?.('mousemove', event), [ - emit, - ]); - const handlePointerEnd = useCallback((event: React.PointerEvent) => emit?.('mouseout', event), [ - emit, - ]); - // if Context or dimensions are not available, wrap self in the needed providers if (!setDimensions) { if (!xScale || !yScale) { @@ -118,10 +152,20 @@ export default function XYChart< ); } + const pointerEventEmitters = usePointerEventEmitters({ source: XYCHART_EVENT_SOURCE }); + const pointerEventHandlers = usePointerEventHandlers({ + dataKey: pointerEvents === 'nearest' ? POINTER_EVENTS_NEAREST : POINTER_EVENTS_ALL, + onPointerMove, + onPointerOut, + onPointerUp, + }); + useEventEmitter('pointermove', pointerEventHandlers.onPointerMove, eventSourceSubscriptions); + useEventEmitter('pointerout', pointerEventHandlers.onPointerOut, eventSourceSubscriptions); + useEventEmitter('pointerup', pointerEventHandlers.onPointerUp, eventSourceSubscriptions); + return width > 0 && height > 0 ? ( {children} - {/** capture all pointer events and emit them. */} {captureEvents && ( )} From 3cc437af7675361b6c449af207a257b4afe689d6 Mon Sep 17 00:00:00 2001 From: Chris Williams Date: Tue, 24 Nov 2020 20:11:16 -0800 Subject: [PATCH 08/24] new(xychart/BaseLineSeries): add pointer handlers, refactor to usePointerEventEmitters/Handlers --- .../series/private/BaseLineSeries.tsx | 75 ++++++++++++------- 1 file changed, 46 insertions(+), 29 deletions(-) diff --git a/packages/visx-xychart/src/components/series/private/BaseLineSeries.tsx b/packages/visx-xychart/src/components/series/private/BaseLineSeries.tsx index 820027e9f..40729cf02 100644 --- a/packages/visx-xychart/src/components/series/private/BaseLineSeries.tsx +++ b/packages/visx-xychart/src/components/series/private/BaseLineSeries.tsx @@ -2,7 +2,7 @@ import React, { useContext, useCallback } from 'react'; import LinePath, { LinePathProps } from '@visx/shape/lib/shapes/LinePath'; import { AxisScale } from '@visx/axis'; import DataContext from '../../../context/DataContext'; -import { SeriesProps } from '../../../types'; +import { PointerEventParams, SeriesProps, TooltipContextType } from '../../../types'; import withRegisteredData, { WithRegisteredDataProps } from '../../../enhancers/withRegisteredData'; import getScaledValueFactory from '../../../utils/getScaledValueFactory'; import useEventEmitter, { HandlerParams } from '../../../hooks/useEventEmitter'; @@ -10,6 +10,9 @@ import findNearestDatumX from '../../../utils/findNearestDatumX'; import TooltipContext from '../../../context/TooltipContext'; import findNearestDatumY from '../../../utils/findNearestDatumY'; import isValidNumber from '../../../typeguards/isValidNumber'; +import { LINESERIES_EVENT_SOURCE, XYCHART_EVENT_SOURCE } from '../../../constants'; +import usePointerEventEmitters from '../../../hooks/usePointerEventEmitters'; +import usePointerEventHandlers from '../../../hooks/usePointerEventHandlers'; export type BaseLineSeriesProps< XScale extends AxisScale, @@ -20,12 +23,21 @@ export type BaseLineSeriesProps< PathComponent?: React.FC, 'ref'>> | 'path'; /** Sets the curve factory (from @visx/curve or d3-curve) for the line generator. Defaults to curveLinear. */ curve?: LinePathProps['curve']; -} & Omit, 'x' | 'y' | 'x0' | 'x1' | 'y0' | 'y1' | 'ref'>; +} & Omit< + React.SVGProps, + 'x' | 'y' | 'x0' | 'x1' | 'y0' | 'y1' | 'ref' | 'pointerEvents' + >; + +const eventSourceSubscriptions = [LINESERIES_EVENT_SOURCE, XYCHART_EVENT_SOURCE]; function BaseLineSeries({ curve, data, dataKey, + onPointerMove: onPointerMoveProps, + onPointerOut: onPointerOutProps, + onPointerUp: onPointerUpProps, + pointerEvents = true, xAccessor, xScale, yAccessor, @@ -33,8 +45,7 @@ function BaseLineSeries & WithRegisteredDataProps) { - const { colorScale, theme, width, height, horizontal } = useContext(DataContext); - const { showTooltip, hideTooltip } = useContext(TooltipContext) ?? {}; + const { colorScale, theme } = useContext(DataContext); const getScaledX = useCallback(getScaledValueFactory(xScale, xAccessor), [xScale, xAccessor]); const getScaledY = useCallback(getScaledValueFactory(yScale, yAccessor), [yScale, yAccessor]); const isDefined = useCallback( @@ -43,33 +54,38 @@ function BaseLineSeries { - const { svgPoint } = params || {}; - if (svgPoint && width && height && showTooltip) { - const datum = (horizontal ? findNearestDatumY : findNearestDatumX)({ - point: svgPoint, - data, - xScale, - yScale, - xAccessor, - yAccessor, - width, - height, - }); - if (datum) { - showTooltip({ - key: dataKey, - ...datum, - svgPoint, - }); - } - } + const { showTooltip, hideTooltip } = (useContext(TooltipContext) ?? {}) as TooltipContextType< + Datum + >; + const onPointerMove = useCallback( + (p: PointerEventParams) => { + showTooltip(p); + if (onPointerMoveProps) onPointerMoveProps(p); + }, + [showTooltip, onPointerMoveProps], + ); + const onPointerOut = useCallback( + (event: React.PointerEvent) => { + hideTooltip(); + if (onPointerOutProps) onPointerOutProps(event); }, - [dataKey, data, xScale, yScale, xAccessor, yAccessor, width, height, showTooltip, horizontal], + [hideTooltip, onPointerOutProps], ); - useEventEmitter('mousemove', handleMouseMove); - useEventEmitter('mouseout', hideTooltip); + const pointerEventEmitters = usePointerEventEmitters({ + source: LINESERIES_EVENT_SOURCE, + onPointerMove: !!onPointerMoveProps, + onPointerOut: !!onPointerOutProps, + onPointerUp: !!onPointerUpProps, + }); + const pointerEventHandlers = usePointerEventHandlers({ + dataKey, + onPointerMove: pointerEvents ? onPointerMove : undefined, + onPointerOut: pointerEvents ? onPointerOut : undefined, + onPointerUp: pointerEvents ? onPointerUpProps : undefined, + }); + useEventEmitter('pointermove', pointerEventHandlers.onPointerMove, eventSourceSubscriptions); + useEventEmitter('pointerout', pointerEventHandlers.onPointerOut, eventSourceSubscriptions); + useEventEmitter('pointerup', pointerEventHandlers.onPointerUp, eventSourceSubscriptions); return ( @@ -80,6 +96,7 @@ function BaseLineSeries )} From 6120dab76db329add3d280f50b5de36df6fe399a Mon Sep 17 00:00:00 2001 From: Chris Williams Date: Tue, 24 Nov 2020 20:11:48 -0800 Subject: [PATCH 09/24] new(xychart/BaseAreaSeries): add pointer handlers, refactor to usePointerEventEmitters/Handlers --- .../series/private/BaseAreaSeries.tsx | 97 ++++++++++++------- 1 file changed, 60 insertions(+), 37 deletions(-) diff --git a/packages/visx-xychart/src/components/series/private/BaseAreaSeries.tsx b/packages/visx-xychart/src/components/series/private/BaseAreaSeries.tsx index e9a336147..3a43e7a8b 100644 --- a/packages/visx-xychart/src/components/series/private/BaseAreaSeries.tsx +++ b/packages/visx-xychart/src/components/series/private/BaseAreaSeries.tsx @@ -3,15 +3,16 @@ import { AxisScale } from '@visx/axis'; import Area, { AreaProps } from '@visx/shape/lib/shapes/Area'; import LinePath, { LinePathProps } from '@visx/shape/lib/shapes/LinePath'; import DataContext from '../../../context/DataContext'; -import { SeriesProps } from '../../../types'; +import { PointerEventParams, SeriesProps, TooltipContextType } from '../../../types'; import withRegisteredData, { WithRegisteredDataProps } from '../../../enhancers/withRegisteredData'; import getScaledValueFactory from '../../../utils/getScaledValueFactory'; -import useEventEmitter, { HandlerParams } from '../../../hooks/useEventEmitter'; -import findNearestDatumX from '../../../utils/findNearestDatumX'; -import TooltipContext from '../../../context/TooltipContext'; -import findNearestDatumY from '../../../utils/findNearestDatumY'; +import useEventEmitter from '../../../hooks/useEventEmitter'; import getScaleBaseline from '../../../utils/getScaleBaseline'; import isValidNumber from '../../../typeguards/isValidNumber'; +import usePointerEventEmitters from '../../../hooks/usePointerEventEmitters'; +import { AREASERIES_EVENT_SOURCE, XYCHART_EVENT_SOURCE } from '../../../constants'; +import usePointerEventHandlers from '../../../hooks/usePointerEventHandlers'; +import TooltipContext from '../../../context/TooltipContext'; export type BaseAreaSeriesProps< XScale extends AxisScale, @@ -26,23 +27,31 @@ export type BaseAreaSeriesProps< lineProps?: Omit, 'data' | 'x' | 'y' | 'children' | 'defined'>; /** Rendered component which is passed path props by BaseAreaSeries after processing. */ PathComponent?: React.FC, 'ref'>> | 'path'; -} & Omit, 'x' | 'y' | 'x0' | 'x1' | 'y0' | 'y1' | 'ref'>; +} & Omit< + React.SVGProps, + 'x' | 'y' | 'x0' | 'x1' | 'y0' | 'y1' | 'ref' | 'pointerEvents' + >; + +const eventSourceSubscriptions = [AREASERIES_EVENT_SOURCE, XYCHART_EVENT_SOURCE]; function BaseAreaSeries({ + PathComponent = 'path', curve, data, dataKey, + lineProps, + onPointerMove: onPointerMoveProps, + onPointerOut: onPointerOutProps, + onPointerUp: onPointerUpProps, + pointerEvents = true, + renderLine = true, xAccessor, xScale, yAccessor, yScale, - renderLine = true, - PathComponent = 'path', - lineProps, ...areaProps }: BaseAreaSeriesProps & WithRegisteredDataProps) { - const { colorScale, theme, width, height, horizontal } = useContext(DataContext); - const { showTooltip, hideTooltip } = useContext(TooltipContext) ?? {}; + const { colorScale, theme, horizontal } = useContext(DataContext); const getScaledX = useCallback(getScaledValueFactory(xScale, xAccessor), [xScale, xAccessor]); const getScaledY = useCallback(getScaledValueFactory(yScale, yAccessor), [yScale, yAccessor]); const isDefined = useCallback( @@ -51,33 +60,38 @@ function BaseAreaSeries { - const { svgPoint } = params || {}; - if (svgPoint && width && height && showTooltip) { - const datum = (horizontal ? findNearestDatumY : findNearestDatumX)({ - point: svgPoint, - data, - xScale, - yScale, - xAccessor, - yAccessor, - width, - height, - }); - if (datum) { - showTooltip({ - key: dataKey, - ...datum, - svgPoint, - }); - } - } + const { showTooltip, hideTooltip } = (useContext(TooltipContext) ?? {}) as TooltipContextType< + Datum + >; + const onPointerMove = useCallback( + (p: PointerEventParams) => { + showTooltip(p); + if (onPointerMoveProps) onPointerMoveProps(p); + }, + [showTooltip, onPointerMoveProps], + ); + const onPointerOut = useCallback( + (event: React.PointerEvent) => { + hideTooltip(); + if (onPointerOutProps) onPointerOutProps(event); }, - [dataKey, data, xScale, yScale, xAccessor, yAccessor, width, height, showTooltip, horizontal], + [hideTooltip, onPointerOutProps], ); - useEventEmitter('mousemove', handleMouseMove); - useEventEmitter('mouseout', hideTooltip); + const pointerEventEmitters = usePointerEventEmitters({ + source: AREASERIES_EVENT_SOURCE, + onPointerMove: !!onPointerMoveProps, + onPointerOut: !!onPointerOutProps, + onPointerUp: !!onPointerUpProps, + }); + const pointerEventHandlers = usePointerEventHandlers({ + dataKey, + onPointerMove: pointerEvents ? onPointerMove : undefined, + onPointerOut: pointerEvents ? onPointerOut : undefined, + onPointerUp: pointerEvents ? onPointerUpProps : undefined, + }); + useEventEmitter('pointermove', pointerEventHandlers.onPointerMove, eventSourceSubscriptions); + useEventEmitter('pointerout', pointerEventHandlers.onPointerOut, eventSourceSubscriptions); + useEventEmitter('pointerup', pointerEventHandlers.onPointerUp, eventSourceSubscriptions); const numericScaleBaseline = useMemo(() => getScaleBaseline(horizontal ? xScale : yScale), [ horizontal, @@ -102,7 +116,14 @@ function BaseAreaSeries {({ path }) => ( - + )} {renderLine && ( @@ -115,9 +136,11 @@ function BaseAreaSeries {({ path }) => ( From 692ebcc4005b0f007b9ba41b5f2bf4607e876e13 Mon Sep 17 00:00:00 2001 From: Chris Williams Date: Tue, 24 Nov 2020 20:13:24 -0800 Subject: [PATCH 10/24] new(xychart/BaseGlyphSeries): add pointer handlers, refactor to usePointerEventEmitters/Handlers --- .../components/series/AnimatedGlyphSeries.tsx | 10 +-- .../src/components/series/GlyphSeries.tsx | 8 +- .../series/private/AnimatedGlyphs.tsx | 6 ++ .../series/private/BaseGlyphSeries.tsx | 87 +++++++++++-------- .../series/private/defaultRenderGlyph.tsx | 17 +++- 5 files changed, 82 insertions(+), 46 deletions(-) diff --git a/packages/visx-xychart/src/components/series/AnimatedGlyphSeries.tsx b/packages/visx-xychart/src/components/series/AnimatedGlyphSeries.tsx index ff29e8ff1..eeffa0257 100644 --- a/packages/visx-xychart/src/components/series/AnimatedGlyphSeries.tsx +++ b/packages/visx-xychart/src/components/series/AnimatedGlyphSeries.tsx @@ -16,14 +16,8 @@ export default function AnimatedGlyphSeries< renderGlyph?: React.FC>; }) { const renderGlyphs = useCallback( - ({ glyphs, xScale, yScale, horizontal }: GlyphsProps) => ( - + (glyphsProps: GlyphsProps) => ( + ), [renderGlyph], ); diff --git a/packages/visx-xychart/src/components/series/GlyphSeries.tsx b/packages/visx-xychart/src/components/series/GlyphSeries.tsx index 342c704aa..1c88b24cb 100644 --- a/packages/visx-xychart/src/components/series/GlyphSeries.tsx +++ b/packages/visx-xychart/src/components/series/GlyphSeries.tsx @@ -15,8 +15,12 @@ export default function GlyphSeries< renderGlyph?: React.FC>; }) { const renderGlyphs = useCallback( - ({ glyphs }: GlyphsProps) => - glyphs.map(glyph => {renderGlyph(glyph)}), + ({ glyphs, onPointerMove, onPointerOut, onPointerUp }: GlyphsProps) => + glyphs.map(glyph => ( + + {renderGlyph({ ...glyph, onPointerMove, onPointerOut, onPointerUp })} + + )), [renderGlyph], ); return ( diff --git a/packages/visx-xychart/src/components/series/private/AnimatedGlyphs.tsx b/packages/visx-xychart/src/components/series/private/AnimatedGlyphs.tsx index 8fc936ba4..0e681c848 100644 --- a/packages/visx-xychart/src/components/series/private/AnimatedGlyphs.tsx +++ b/packages/visx-xychart/src/components/series/private/AnimatedGlyphs.tsx @@ -56,6 +56,9 @@ export default function AnimatedGlyphs< horizontal, xScale, yScale, + onPointerMove, + onPointerOut, + onPointerUp, }: { // unanimated Glyph component renderGlyph: React.FC>; @@ -87,6 +90,9 @@ export default function AnimatedGlyphs< y: 0, size: item.size, color: 'currentColor', // allows us to animate the color of the element + onPointerMove, + onPointerOut, + onPointerUp, })} ))} diff --git a/packages/visx-xychart/src/components/series/private/BaseGlyphSeries.tsx b/packages/visx-xychart/src/components/series/private/BaseGlyphSeries.tsx index fc80984c1..a33dd530f 100644 --- a/packages/visx-xychart/src/components/series/private/BaseGlyphSeries.tsx +++ b/packages/visx-xychart/src/components/series/private/BaseGlyphSeries.tsx @@ -1,14 +1,21 @@ import React, { useContext, useCallback, useMemo } from 'react'; import { AxisScale } from '@visx/axis'; import DataContext from '../../../context/DataContext'; -import { GlyphProps, GlyphsProps, SeriesProps } from '../../../types'; +import { + GlyphProps, + GlyphsProps, + PointerEventParams, + SeriesProps, + TooltipContextType, +} from '../../../types'; import withRegisteredData, { WithRegisteredDataProps } from '../../../enhancers/withRegisteredData'; import getScaledValueFactory from '../../../utils/getScaledValueFactory'; -import useEventEmitter, { HandlerParams } from '../../../hooks/useEventEmitter'; -import findNearestDatumX from '../../../utils/findNearestDatumX'; -import TooltipContext from '../../../context/TooltipContext'; -import findNearestDatumY from '../../../utils/findNearestDatumY'; +import useEventEmitter from '../../../hooks/useEventEmitter'; import isValidNumber from '../../../typeguards/isValidNumber'; +import usePointerEventEmitters from '../../../hooks/usePointerEventEmitters'; +import { GLYPHSERIES_EVENT_SOURCE, XYCHART_EVENT_SOURCE } from '../../../constants'; +import usePointerEventHandlers from '../../../hooks/usePointerEventHandlers'; +import TooltipContext from '../../../context/TooltipContext'; export type BaseGlyphSeriesProps< XScale extends AxisScale, @@ -21,50 +28,60 @@ export type BaseGlyphSeriesProps< renderGlyphs: (glyphsProps: GlyphsProps) => React.ReactNode; }; +const eventSourceSubscriptions = [XYCHART_EVENT_SOURCE, GLYPHSERIES_EVENT_SOURCE]; + function BaseGlyphSeries({ data, dataKey, + onPointerMove: onPointerMoveProps, + onPointerOut: onPointerOutProps, + onPointerUp: onPointerUpProps, + pointerEvents = true, + renderGlyphs, + size = 8, xAccessor, xScale, yAccessor, yScale, - size = 8, - renderGlyphs, }: BaseGlyphSeriesProps & WithRegisteredDataProps) { - const { colorScale, theme, width, height, horizontal } = useContext(DataContext); - const { showTooltip, hideTooltip } = useContext(TooltipContext) ?? {}; + const { colorScale, theme, horizontal } = useContext(DataContext); const getScaledX = useCallback(getScaledValueFactory(xScale, xAccessor), [xScale, xAccessor]); const getScaledY = useCallback(getScaledValueFactory(yScale, yAccessor), [yScale, yAccessor]); // @TODO allow override const color = colorScale?.(dataKey) ?? theme?.colors?.[0] ?? '#222'; - const handleMouseMove = useCallback( - (params?: HandlerParams) => { - const { svgPoint } = params || {}; - if (svgPoint && width && height && showTooltip) { - const datum = (horizontal ? findNearestDatumY : findNearestDatumX)({ - point: svgPoint, - data, - xScale, - yScale, - xAccessor, - yAccessor, - width, - height, - }); - if (datum) { - showTooltip({ - key: dataKey, - ...datum, - svgPoint, - }); - } - } + const { showTooltip, hideTooltip } = (useContext(TooltipContext) ?? {}) as TooltipContextType< + Datum + >; + const onPointerMove = useCallback( + (p: PointerEventParams) => { + showTooltip(p); + if (onPointerMoveProps) onPointerMoveProps(p); + }, + [showTooltip, onPointerMoveProps], + ); + const onPointerOut = useCallback( + (event: React.PointerEvent) => { + hideTooltip(); + if (onPointerOutProps) onPointerOutProps(event); }, - [dataKey, data, xScale, yScale, xAccessor, yAccessor, width, height, showTooltip, horizontal], + [hideTooltip, onPointerOutProps], ); - useEventEmitter('mousemove', handleMouseMove); - useEventEmitter('mouseout', hideTooltip); + const pointerEventEmitters = usePointerEventEmitters({ + source: GLYPHSERIES_EVENT_SOURCE, + onPointerMove: !!onPointerMoveProps, + onPointerOut: !!onPointerOutProps, + onPointerUp: !!onPointerUpProps, + }); + const pointerEventHandlers = usePointerEventHandlers({ + dataKey, + onPointerMove: pointerEvents ? onPointerMove : undefined, + onPointerOut: pointerEvents ? onPointerOut : undefined, + onPointerUp: pointerEvents ? onPointerUpProps : undefined, + }); + useEventEmitter('pointermove', pointerEventHandlers.onPointerMove, eventSourceSubscriptions); + useEventEmitter('pointerout', pointerEventHandlers.onPointerOut, eventSourceSubscriptions); + useEventEmitter('pointerup', pointerEventHandlers.onPointerUp, eventSourceSubscriptions); const glyphs = useMemo( () => @@ -89,7 +106,7 @@ function BaseGlyphSeries{renderGlyphs({ glyphs, xScale, yScale, horizontal })} + <>{renderGlyphs({ glyphs, xScale, yScale, horizontal, ...pointerEventEmitters })} ); } diff --git a/packages/visx-xychart/src/components/series/private/defaultRenderGlyph.tsx b/packages/visx-xychart/src/components/series/private/defaultRenderGlyph.tsx index 822bbb48f..fced8dbb5 100644 --- a/packages/visx-xychart/src/components/series/private/defaultRenderGlyph.tsx +++ b/packages/visx-xychart/src/components/series/private/defaultRenderGlyph.tsx @@ -7,6 +7,21 @@ export default function defaultRenderGlyph({ x, y, size, + onPointerMove, + onPointerOut, + onPointerUp, }: GlyphProps) { - return ; + return ( + + ); } From 5cfd82b87aafd6b601abd8206a2cfc71b7b14274 Mon Sep 17 00:00:00 2001 From: Chris Williams Date: Tue, 24 Nov 2020 20:14:22 -0800 Subject: [PATCH 11/24] new(xychart/BaseBarSeries): add pointer handlers, refactor to usePointerEventEmitters/Handlers --- .../series/private/AnimatedBars.tsx | 1 + .../src/components/series/private/Bars.tsx | 2 +- .../series/private/BaseBarSeries.tsx | 101 ++++++++++-------- 3 files changed, 61 insertions(+), 43 deletions(-) diff --git a/packages/visx-xychart/src/components/series/private/AnimatedBars.tsx b/packages/visx-xychart/src/components/series/private/AnimatedBars.tsx index 84a497e65..07e32b564 100644 --- a/packages/visx-xychart/src/components/series/private/AnimatedBars.tsx +++ b/packages/visx-xychart/src/components/series/private/AnimatedBars.tsx @@ -65,6 +65,7 @@ export default function AnimatedBars {bars.map(({ key, ...barProps }) => ( - + ))} ); diff --git a/packages/visx-xychart/src/components/series/private/BaseBarSeries.tsx b/packages/visx-xychart/src/components/series/private/BaseBarSeries.tsx index 9d5c0b0ef..e3966374f 100644 --- a/packages/visx-xychart/src/components/series/private/BaseBarSeries.tsx +++ b/packages/visx-xychart/src/components/series/private/BaseBarSeries.tsx @@ -1,16 +1,23 @@ import React, { useContext, useCallback, useMemo } from 'react'; import { AxisScale } from '@visx/axis'; import DataContext from '../../../context/DataContext'; -import { Bar, BarsProps, SeriesProps } from '../../../types'; +import { + Bar, + BarsProps, + PointerEventParams, + SeriesProps, + TooltipContextType, +} from '../../../types'; import withRegisteredData, { WithRegisteredDataProps } from '../../../enhancers/withRegisteredData'; import getScaledValueFactory from '../../../utils/getScaledValueFactory'; import getScaleBandwidth from '../../../utils/getScaleBandwidth'; -import findNearestDatumX from '../../../utils/findNearestDatumX'; -import findNearestDatumY from '../../../utils/findNearestDatumY'; -import useEventEmitter, { HandlerParams } from '../../../hooks/useEventEmitter'; -import TooltipContext from '../../../context/TooltipContext'; +import useEventEmitter from '../../../hooks/useEventEmitter'; import getScaleBaseline from '../../../utils/getScaleBaseline'; import isValidNumber from '../../../typeguards/isValidNumber'; +import { BARSERIES_EVENT_SOURCE, XYCHART_EVENT_SOURCE } from '../../../constants'; +import usePointerEventEmitters from '../../../hooks/usePointerEventEmitters'; +import usePointerEventHandlers from '../../../hooks/usePointerEventHandlers'; +import TooltipContext from '../../../context/TooltipContext'; export type BaseBarSeriesProps< XScale extends AxisScale, @@ -26,30 +33,30 @@ export type BaseBarSeriesProps< barPadding?: number; }; +const eventSourceSubscriptions = [XYCHART_EVENT_SOURCE, BARSERIES_EVENT_SOURCE]; + // Fallback bandwidth estimate assumes no missing data values (divides chart space by # datum) const getFallbackBandwidth = (fullBarWidth: number, barPadding: number) => // clamp padding to [0, 1], bar thickness = (1-padding) * availableSpace fullBarWidth * (1 - Math.min(1, Math.max(0, barPadding))); function BaseBarSeries({ - barPadding = 0.1, BarsComponent, + barPadding = 0.1, data, dataKey, + onPointerMove: onPointerMoveProps, + onPointerOut: onPointerOutProps, + onPointerUp: onPointerUpProps, + pointerEvents = true, xAccessor, xScale, yAccessor, yScale, }: BaseBarSeriesProps & WithRegisteredDataProps) { - const { - colorScale, - horizontal, - theme, - width, - height, - innerWidth = 0, - innerHeight = 0, - } = useContext(DataContext); + const { colorScale, horizontal, theme, innerWidth = 0, innerHeight = 0 } = useContext( + DataContext, + ); const getScaledX = useCallback(getScaledValueFactory(xScale, xAccessor), [xScale, xAccessor]); const getScaledY = useCallback(getScaledValueFactory(yScale, yAccessor), [yScale, yAccessor]); const scaleBandwidth = getScaleBandwidth(horizontal ? yScale : xScale); @@ -86,38 +93,48 @@ function BaseBarSeries bar) as Bar[]; }, [barThickness, color, data, getScaledX, getScaledY, horizontal, xZeroPosition, yZeroPosition]); - const { showTooltip, hideTooltip } = useContext(TooltipContext) ?? {}; - const handleMouseMove = useCallback( - (params?: HandlerParams) => { - const { svgPoint } = params || {}; - if (svgPoint && width && height && showTooltip) { - const datum = (horizontal ? findNearestDatumY : findNearestDatumX)({ - point: svgPoint, - data, - xScale, - yScale, - xAccessor, - yAccessor, - width, - height, - }); - if (datum) { - showTooltip({ - key: dataKey, - ...datum, - svgPoint, - }); - } - } + const { showTooltip, hideTooltip } = (useContext(TooltipContext) ?? {}) as TooltipContextType< + Datum + >; + const onPointerMove = useCallback( + (p: PointerEventParams) => { + showTooltip(p); + if (onPointerMoveProps) onPointerMoveProps(p); + }, + [showTooltip, onPointerMoveProps], + ); + const onPointerOut = useCallback( + (event: React.PointerEvent) => { + hideTooltip(); + if (onPointerOutProps) onPointerOutProps(event); }, - [dataKey, data, horizontal, xScale, yScale, xAccessor, yAccessor, width, height, showTooltip], + [hideTooltip, onPointerOutProps], ); - useEventEmitter('mousemove', handleMouseMove); - useEventEmitter('mouseout', hideTooltip); + const pointerEventEmitters = usePointerEventEmitters({ + source: BARSERIES_EVENT_SOURCE, + onPointerMove: !!onPointerMoveProps, + onPointerOut: !!onPointerOutProps, + onPointerUp: !!onPointerUpProps, + }); + const pointerEventHandlers = usePointerEventHandlers({ + dataKey, + onPointerMove: pointerEvents ? onPointerMove : undefined, + onPointerOut: pointerEvents ? onPointerOut : undefined, + onPointerUp: pointerEvents ? onPointerUpProps : undefined, + }); + useEventEmitter('pointermove', pointerEventHandlers.onPointerMove, eventSourceSubscriptions); + useEventEmitter('pointerout', pointerEventHandlers.onPointerOut, eventSourceSubscriptions); + useEventEmitter('pointerup', pointerEventHandlers.onPointerUp, eventSourceSubscriptions); return ( - + ); } From 5e233972c7610bef1abd5f917770c9c956d4d5f8 Mon Sep 17 00:00:00 2001 From: Chris Williams Date: Tue, 24 Nov 2020 20:14:44 -0800 Subject: [PATCH 12/24] neww(xychart/BaseBarGroup): add pointer handlers, refactor to usePointerEventEmitters/Handlers --- .../series/private/BaseBarGroup.tsx | 123 +++++++++++------- 1 file changed, 77 insertions(+), 46 deletions(-) diff --git a/packages/visx-xychart/src/components/series/private/BaseBarGroup.tsx b/packages/visx-xychart/src/components/series/private/BaseBarGroup.tsx index 05aa0b578..b4eb4ae17 100644 --- a/packages/visx-xychart/src/components/series/private/BaseBarGroup.tsx +++ b/packages/visx-xychart/src/components/series/private/BaseBarGroup.tsx @@ -1,19 +1,31 @@ -import React, { useContext, useCallback, useMemo, useEffect } from 'react'; +import React, { useContext, useMemo, useEffect, useCallback } from 'react'; import { PositionScale } from '@visx/shape/lib/types'; import { scaleBand } from '@visx/scale'; import isChildWithProps from '../../../typeguards/isChildWithProps'; import { BaseBarSeriesProps } from './BaseBarSeries'; -import { Bar, BarsProps, DataContextType } from '../../../types'; +import { + Bar, + BarsProps, + DataContextType, + PointerEventParams, + SeriesProps, + TooltipContextType, +} from '../../../types'; import DataContext from '../../../context/DataContext'; import getScaleBandwidth from '../../../utils/getScaleBandwidth'; -import findNearestDatumY from '../../../utils/findNearestDatumY'; -import findNearestDatumX from '../../../utils/findNearestDatumX'; -import useEventEmitter, { HandlerParams } from '../../../hooks/useEventEmitter'; -import TooltipContext from '../../../context/TooltipContext'; +import useEventEmitter from '../../../hooks/useEventEmitter'; import getScaleBaseline from '../../../utils/getScaleBaseline'; import isValidNumber from '../../../typeguards/isValidNumber'; +import { BARGROUP_EVENT_SOURCE, XYCHART_EVENT_SOURCE } from '../../../constants'; +import usePointerEventEmitters from '../../../hooks/usePointerEventEmitters'; +import usePointerEventHandlers from '../../../hooks/usePointerEventHandlers'; +import TooltipContext from '../../../context/TooltipContext'; -export type BaseBarGroupProps = { +export type BaseBarGroupProps< + XScale extends PositionScale, + YScale extends PositionScale, + Datum extends object +> = { /** `BarSeries` elements */ children: JSX.Element | JSX.Element[]; /** Group band scale padding, [0, 1] where 0 = no padding, 1 = no bar. */ @@ -22,23 +34,37 @@ export type BaseBarGroupProps number; /** Rendered component which is passed BarsProps by BaseBarGroup after processing. */ BarsComponent: React.FC>; -}; +} & Pick< + SeriesProps, + 'onPointerMove' | 'onPointerOut' | 'onPointerUp' | 'pointerEvents' +>; + +// in the future, if BarGroup preserves pointer events from child BarSeries, +// we could add e.g., `${BASEBARGROUP_EVENT_SOURCE}-${dataKey}` to this in render fn. +const eventSourceSubscriptions = [XYCHART_EVENT_SOURCE, BARGROUP_EVENT_SOURCE]; export default function BaseBarGroup< XScale extends PositionScale, YScale extends PositionScale, Datum extends object ->({ children, padding = 0.1, sortBars, BarsComponent }: BaseBarGroupProps) { +>({ + children, + padding = 0.1, + sortBars, + BarsComponent, + onPointerMove: onPointerMoveProps, + onPointerOut: onPointerOutProps, + onPointerUp: onPointerUpProps, + pointerEvents = true, +}: BaseBarGroupProps) { const { - xScale, - yScale, colorScale, dataRegistry, + horizontal, registerData, unregisterData, - width, - height, - horizontal, + xScale, + yScale, } = (useContext(DataContext) as unknown) as DataContextType; const barSeriesChildren = useMemo( @@ -77,39 +103,38 @@ export default function BaseBarGroup< [sortBars, dataKeys, xScale, yScale, horizontal, padding], ); - const { showTooltip, hideTooltip } = useContext(TooltipContext) ?? {}; - const handleMouseMove = useCallback( - (params?: HandlerParams) => { - const { svgPoint } = params || {}; - // invoke showTooltip for each key so all data is available in context, - // and let Tooltip find the nearest point among them - dataKeys.forEach(key => { - const entry = dataRegistry.get(key); - if (entry && svgPoint && width && height && showTooltip) { - const datum = (horizontal ? findNearestDatumY : findNearestDatumX)({ - point: svgPoint, - data: entry.data, - xScale, - yScale, - xAccessor: entry.xAccessor, - yAccessor: entry.yAccessor, - width, - height, - }); - if (datum) { - showTooltip({ - key, - ...datum, - svgPoint, - }); - } - } - }); + const { showTooltip, hideTooltip } = (useContext(TooltipContext) ?? {}) as TooltipContextType< + Datum + >; + const onPointerMove = useCallback( + (p: PointerEventParams) => { + showTooltip(p); + if (onPointerMoveProps) onPointerMoveProps(p); }, - [dataKeys, dataRegistry, horizontal, xScale, yScale, width, height, showTooltip], + [showTooltip, onPointerMoveProps], ); - useEventEmitter('mousemove', handleMouseMove); - useEventEmitter('mouseout', hideTooltip); + const onPointerOut = useCallback( + (event: React.PointerEvent) => { + hideTooltip(); + if (onPointerOutProps) onPointerOutProps(event); + }, + [hideTooltip, onPointerOutProps], + ); + const pointerEventEmitters = usePointerEventEmitters({ + source: BARGROUP_EVENT_SOURCE, + onPointerMove: !!onPointerMoveProps, + onPointerOut: !!onPointerOutProps, + onPointerUp: !!onPointerUpProps, + }); + const pointerEventHandlers = usePointerEventHandlers({ + dataKey: dataKeys, + onPointerMove: pointerEvents ? onPointerMove : undefined, + onPointerOut: pointerEvents ? onPointerOut : undefined, + onPointerUp: pointerEvents ? onPointerUpProps : undefined, + }); + useEventEmitter('pointermove', pointerEventHandlers.onPointerMove, eventSourceSubscriptions); + useEventEmitter('pointerout', pointerEventHandlers.onPointerOut, eventSourceSubscriptions); + useEventEmitter('pointerup', pointerEventHandlers.onPointerUp, eventSourceSubscriptions); const xZeroPosition = useMemo(() => (xScale ? getScaleBaseline(xScale) : 0), [xScale]); const yZeroPosition = useMemo(() => (yScale ? getScaleBaseline(yScale) : 0), [yScale]); @@ -171,7 +196,13 @@ export default function BaseBarGroup< return ( - + ); } From 1127da46b192216bbd4943f942b57a75c05c005e Mon Sep 17 00:00:00 2001 From: Chris Williams Date: Tue, 24 Nov 2020 20:15:01 -0800 Subject: [PATCH 13/24] neww(xychart/BaseBarStack): add pointer handlers, refactor to usePointerEventEmitters/Handlers --- .../series/private/BaseBarStack.tsx | 136 ++++++++++-------- 1 file changed, 74 insertions(+), 62 deletions(-) diff --git a/packages/visx-xychart/src/components/series/private/BaseBarStack.tsx b/packages/visx-xychart/src/components/series/private/BaseBarStack.tsx index 70cb17ea6..8f9a46bef 100644 --- a/packages/visx-xychart/src/components/series/private/BaseBarStack.tsx +++ b/packages/visx-xychart/src/components/series/private/BaseBarStack.tsx @@ -9,14 +9,25 @@ import { extent } from 'd3-array'; import getBandwidth from '@visx/shape/lib/util/getBandwidth'; import { BaseBarSeriesProps } from './BaseBarSeries'; import DataContext from '../../../context/DataContext'; -import { Bar, BarsProps, BarStackDatum, CombinedStackData, DataContextType } from '../../../types'; -import TooltipContext from '../../../context/TooltipContext'; -import useEventEmitter, { HandlerParams } from '../../../hooks/useEventEmitter'; +import { + Bar, + BarsProps, + BarStackDatum, + CombinedStackData, + DataContextType, + PointerEventParams, + SeriesProps, + TooltipContextType, +} from '../../../types'; +import useEventEmitter from '../../../hooks/useEventEmitter'; import isValidNumber from '../../../typeguards/isValidNumber'; import isChildWithProps from '../../../typeguards/isChildWithProps'; import combineBarBarStackData, { getStackValue } from '../../../utils/combineBarStackData'; import getBarStackRegistryData from '../../../utils/getBarStackRegistryData'; -import findNearestStackDatum from '../../../utils/findNearestStackDatum'; +import usePointerEventEmitters from '../../../hooks/usePointerEventEmitters'; +import { BARSTACK_EVENT_SOURCE, XYCHART_EVENT_SOURCE } from '../../../constants'; +import usePointerEventHandlers from '../../../hooks/usePointerEventHandlers'; +import TooltipContext from '../../../context/TooltipContext'; export type BaseBarStackProps< XScale extends PositionScale, @@ -27,24 +38,39 @@ export type BaseBarStackProps< children: JSX.Element | JSX.Element[]; /** Rendered component which is passed BarsProps by BaseBarStack after processing. */ BarsComponent: React.FC>; -} & Pick, 'offset' | 'order'>; +} & Pick, 'offset' | 'order'> & + Pick< + SeriesProps, + 'onPointerMove' | 'onPointerOut' | 'onPointerUp' | 'pointerEvents' + >; + +// in the future, if BarGroup preserves pointer events from child BarSeries, +// we could add e.g., `${BASEBARGROUP_EVENT_SOURCE}-${dataKey}` to this in render fn. +const eventSourceSubscriptions = [XYCHART_EVENT_SOURCE, BARSTACK_EVENT_SOURCE]; function BaseBarStack< XScale extends PositionScale, YScale extends PositionScale, Datum extends object ->({ children, order, offset, BarsComponent }: BaseBarStackProps) { +>({ + children, + order, + offset, + BarsComponent, + onPointerMove: onPointerMoveProps, + onPointerOut: onPointerOutProps, + onPointerUp: onPointerUpProps, + pointerEvents = true, +}: BaseBarStackProps) { type StackBar = SeriesPoint>; const { - xScale, - yScale, colorScale, dataRegistry, + horizontal, registerData, unregisterData, - width, - height, - horizontal, + xScale, + yScale, } = (useContext(DataContext) as unknown) as DataContextType< XScale, YScale, @@ -113,58 +139,38 @@ function BaseBarStack< barSeriesChildren, ]); - // register mouse listeners - const { showTooltip, hideTooltip } = useContext(TooltipContext) ?? {}; - - const handleMouseMove = useCallback( - (params: HandlerParams | undefined) => { - const { svgPoint } = params || {}; - - // invoke showTooltip for each key so all data is available in context, - // and let Tooltip find the nearest point among them - dataKeys.forEach(key => { - const entry = dataRegistry.get(key); - const childData = barSeriesChildren.find(child => child.props.dataKey === key)?.props.data; - if (childData && svgPoint && width && height && showTooltip) { - const datum = findNearestStackDatum( - { - point: svgPoint, - data: entry.data, - xScale, - yScale, - xAccessor: entry.xAccessor, - yAccessor: entry.yAccessor, - width, - height, - }, - childData, - horizontal, - ); - - if (datum) { - showTooltip({ - key, - svgPoint, - ...datum, - }); - } - } - }); + const { showTooltip, hideTooltip } = (useContext(TooltipContext) ?? {}) as TooltipContextType< + Datum + >; + const onPointerMove = useCallback( + (p: PointerEventParams) => { + showTooltip(p); + if (onPointerMoveProps) onPointerMoveProps(p); + }, + [showTooltip, onPointerMoveProps], + ); + const onPointerOut = useCallback( + (event: React.PointerEvent) => { + hideTooltip(); + if (onPointerOutProps) onPointerOutProps(event); }, - [ - barSeriesChildren, - dataRegistry, - dataKeys, - horizontal, - xScale, - yScale, - width, - height, - showTooltip, - ], + [hideTooltip, onPointerOutProps], ); - useEventEmitter('mousemove', handleMouseMove); - useEventEmitter('mouseout', hideTooltip); + const pointerEventEmitters = usePointerEventEmitters({ + source: BARSTACK_EVENT_SOURCE, + onPointerMove: !!onPointerMoveProps, + onPointerOut: !!onPointerOutProps, + onPointerUp: !!onPointerUpProps, + }); + const pointerEventHandlers = usePointerEventHandlers({ + dataKey: dataKeys, + onPointerMove: pointerEvents ? onPointerMove : undefined, + onPointerOut: pointerEvents ? onPointerOut : undefined, + onPointerUp: pointerEvents ? onPointerUpProps : undefined, + }); + useEventEmitter('pointermove', pointerEventHandlers.onPointerMove, eventSourceSubscriptions); + useEventEmitter('pointerout', pointerEventHandlers.onPointerOut, eventSourceSubscriptions); + useEventEmitter('pointerup', pointerEventHandlers.onPointerUp, eventSourceSubscriptions); // if scales and data are not available in the registry, bail if (dataKeys.some(key => dataRegistry.get(key) == null) || !xScale || !yScale || !colorScale) { @@ -226,7 +232,13 @@ function BaseBarStack< return ( - + ); } From 01be76099537f900ad7ceeb70d12803f11f617c9 Mon Sep 17 00:00:00 2001 From: Chris Williams Date: Tue, 24 Nov 2020 20:15:24 -0800 Subject: [PATCH 14/24] new(xychart/AnimatedPath): add className --- .../visx-xychart/src/components/series/private/AnimatedPath.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/visx-xychart/src/components/series/private/AnimatedPath.tsx b/packages/visx-xychart/src/components/series/private/AnimatedPath.tsx index 7740a8cfe..665003bc3 100644 --- a/packages/visx-xychart/src/components/series/private/AnimatedPath.tsx +++ b/packages/visx-xychart/src/components/series/private/AnimatedPath.tsx @@ -24,6 +24,7 @@ export default function AnimatedPath({ const tweened = useSpring({ stroke, fill }); return ( Date: Tue, 24 Nov 2020 20:16:04 -0800 Subject: [PATCH 15/24] new(demo/xychart): add onPointerUp example to demo --- .../src/sandboxes/visx-xychart/Example.tsx | 6 ++++++ .../sandboxes/visx-xychart/ExampleControls.tsx | 18 +++++++++++------- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/packages/visx-demo/src/sandboxes/visx-xychart/Example.tsx b/packages/visx-demo/src/sandboxes/visx-xychart/Example.tsx index 52c2b90b3..2aa06d79b 100644 --- a/packages/visx-demo/src/sandboxes/visx-xychart/Example.tsx +++ b/packages/visx-demo/src/sandboxes/visx-xychart/Example.tsx @@ -50,6 +50,8 @@ export default function Example({ height }: Props) { renderGlyphSeries, renderHorizontally, renderLineSeries, + setAnnotationDataIndex, + setAnnotationDataKey, setAnnotationLabelPosition, sharedTooltip, showGridColumns, @@ -69,6 +71,10 @@ export default function Example({ height }: Props) { yScale={config.y} height={Math.min(400, height)} captureEvents={!editAnnotationLabelPosition} + onPointerUp={d => { + setAnnotationDataKey(d.key); + setAnnotationDataIndex(d.index); + }} > ('linear'); const themeBackground = theme.backgroundColor; const renderGlyph = useCallback( - ({ size, color }: GlyphProps) => { + ({ size, color, onPointerMove, onPointerOut, onPointerUp }: GlyphProps) => { + const handlers = { onPointerMove, onPointerOut, onPointerUp }; if (glyphComponent === 'star') { - return ; + return ; } if (glyphComponent === 'circle') { - return ; + return ; } if (glyphComponent === 'cross') { - return ; + return ; } return ( - + 🍍 ); @@ -176,7 +178,7 @@ export default function ExampleControls({ children }: ControlsProps) { accessors, animationTrajectory, annotationDataKey, - annotationDatum: data[defaultAnnotationDataIndex], + annotationDatum: data[annotationDataIndex], annotationLabelPosition, annotationType, config, @@ -201,6 +203,8 @@ export default function ExampleControls({ children }: ControlsProps) { renderHorizontally, renderAreaSeries: canRenderLineOrArea && renderLineOrAreaSeries === 'area', renderLineSeries: canRenderLineOrArea && renderLineOrAreaSeries === 'line', + setAnnotationDataIndex, + setAnnotationDataKey, setAnnotationLabelPosition, sharedTooltip, showGridColumns, @@ -500,7 +504,7 @@ export default function ExampleControls({ children }: ControlsProps) { {/** annotation */}
- annotation + annotation (click chart to update)
{/** annotation */}
- annotation (click chart to update) + annotation (click chart to update)