Skip to content

Commit

Permalink
new(xychart): add PointerEvent handlers to XYChart and *Series (#947)
Browse files Browse the repository at this point in the history
* new(xychart): add event source constants

* new(xychart): add hooks/usePointerEventEmitters

* new(xychart): add hooks/usePointerEventHandlers

* new(xychart/useEventEmitter): update to pointer events, add prop annotations, add source filtering

* internal(xychart/findNearestDatum): factor out return type to types

* types(xychart): add pointer handlers to series types

* new(xychart/XYChart): add pointer handlers, refactor to usePointerEventEmitters/Handlers

* new(xychart/BaseLineSeries): add pointer handlers, refactor to usePointerEventEmitters/Handlers

* new(xychart/BaseAreaSeries): add pointer handlers, refactor to usePointerEventEmitters/Handlers

* new(xychart/BaseGlyphSeries): add pointer handlers, refactor to usePointerEventEmitters/Handlers

* new(xychart/BaseBarSeries): add pointer handlers, refactor to usePointerEventEmitters/Handlers

* neww(xychart/BaseBarGroup): add pointer handlers, refactor to usePointerEventEmitters/Handlers

* neww(xychart/BaseBarStack): add pointer handlers, refactor to usePointerEventEmitters/Handlers

* new(xychart/AnimatedPath): add className

* new(demo/xychart): add onPointerUp example to demo

* fix(xychart): consider pointerEvents for Series pointer event emitter creation

* fix(demo/xychart): add setAnnotationDataIndex, setAnnotationDataKey to ProvidedProps

* internal(xychart): move useEventEmitter calls to usePointerEventHandlers; use dataKeys in event sources

* api(xychart/XYChart): rename prop pointerEvents => pointerEventsDataKey

* internal(xychart/usePointerEventHandlers): fix comment typo

* fix(xychart/useEventEmitter): mousemove => pointermove

* test(xychart): update mousemove/out => pointermove/out

* type(xychart): fix types

* test(xychart): add event source to fix Series tests
  • Loading branch information
williaster authored Dec 3, 2020
1 parent b6a455e commit dd11a5c
Show file tree
Hide file tree
Showing 36 changed files with 777 additions and 360 deletions.
6 changes: 6 additions & 0 deletions packages/visx-demo/src/sandboxes/visx-xychart/Example.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ export default function Example({ height }: Props) {
renderGlyphSeries,
renderHorizontally,
renderLineSeries,
setAnnotationDataIndex,
setAnnotationDataKey,
setAnnotationLabelPosition,
sharedTooltip,
showGridColumns,
Expand All @@ -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 as 'New York' | 'San Francisco' | 'Austin');
setAnnotationDataIndex(d.index);
}}
>
<CustomChartBackground />
<AnimatedGrid
Expand Down
20 changes: 13 additions & 7 deletions packages/visx-demo/src/sandboxes/visx-xychart/ExampleControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ type ProvidedProps = {
data: CityTemperature[];
editAnnotationLabelPosition: boolean;
numTicks: number;
setAnnotationDataIndex: (index: number) => void;
setAnnotationDataKey: (key: keyof Accessors | null) => void;
setAnnotationLabelPosition: (position: { dx: number; dy: number }) => void;
renderAreaSeries: boolean;
renderBarGroup: boolean;
Expand Down Expand Up @@ -109,25 +111,27 @@ export default function ExampleControls({ children }: ControlsProps) {
const [renderGlyphSeries, setRenderGlyphSeries] = useState(false);
const [editAnnotationLabelPosition, setEditAnnotationLabelPosition] = useState(false);
const [annotationLabelPosition, setAnnotationLabelPosition] = useState({ dx: -40, dy: -20 });
const [annotationDataIndex, setAnnotationDataIndex] = useState(defaultAnnotationDataIndex);
const [negativeValues, setNegativeValues] = useState(false);
const [fewerDatum, setFewerDatum] = useState(false);
const [missingValues, setMissingValues] = useState(false);
const [glyphComponent, setGlyphComponent] = useState<'star' | 'cross' | 'circle' | '🍍'>('star');
const [curveType, setCurveType] = useState<'linear' | 'cardinal' | 'step'>('linear');
const themeBackground = theme.backgroundColor;
const renderGlyph = useCallback(
({ size, color }: GlyphProps<CityTemperature>) => {
({ size, color, onPointerMove, onPointerOut, onPointerUp }: GlyphProps<CityTemperature>) => {
const handlers = { onPointerMove, onPointerOut, onPointerUp };
if (glyphComponent === 'star') {
return <GlyphStar stroke={themeBackground} fill={color} size={size * 8} />;
return <GlyphStar stroke={themeBackground} fill={color} size={size * 8} {...handlers} />;
}
if (glyphComponent === 'circle') {
return <GlyphDot stroke={themeBackground} fill={color} r={size / 2} />;
return <GlyphDot stroke={themeBackground} fill={color} r={size / 2} {...handlers} />;
}
if (glyphComponent === 'cross') {
return <GlyphCross stroke={themeBackground} fill={color} size={size * 8} />;
return <GlyphCross stroke={themeBackground} fill={color} size={size * 8} {...handlers} />;
}
return (
<text dx="-0.75em" dy="0.25em" fontSize={14}>
<text dx="-0.75em" dy="0.25em" fontSize={14} {...handlers}>
🍍
</text>
);
Expand Down Expand Up @@ -176,7 +180,7 @@ export default function ExampleControls({ children }: ControlsProps) {
accessors,
animationTrajectory,
annotationDataKey,
annotationDatum: data[defaultAnnotationDataIndex],
annotationDatum: data[annotationDataIndex],
annotationLabelPosition,
annotationType,
config,
Expand All @@ -201,6 +205,8 @@ export default function ExampleControls({ children }: ControlsProps) {
renderHorizontally,
renderAreaSeries: canRenderLineOrArea && renderLineOrAreaSeries === 'area',
renderLineSeries: canRenderLineOrArea && renderLineOrAreaSeries === 'line',
setAnnotationDataIndex,
setAnnotationDataKey,
setAnnotationLabelPosition,
sharedTooltip,
showGridColumns,
Expand Down Expand Up @@ -500,7 +506,7 @@ export default function ExampleControls({ children }: ControlsProps) {
</div>
{/** annotation */}
<div>
<strong>annotation</strong>
<strong>annotation</strong> (click chart to update)
<label>
<input
type="radio"
Expand Down
71 changes: 56 additions & 15 deletions packages/visx-xychart/src/components/XYChart.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,33 @@
/* eslint jsx-a11y/mouse-events-have-key-events: 'off', @typescript-eslint/no-explicit-any: 'off' */
import React, { useCallback, useContext, useEffect } from 'react';
import React, { useContext, useEffect } from 'react';
import ParentSize from '@visx/responsive/lib/components/ParentSize';
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<AxisScaleOutput, any, any>,
YScaleConfig extends ScaleConfig<AxisScaleOutput, any, any>
YScaleConfig extends ScaleConfig<AxisScaleOutput, any, any>,
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;
Expand All @@ -36,18 +43,52 @@ export type XYChartProps<
xScale?: DataProviderProps<XScaleConfig, YScaleConfig>['xScale'];
/** If DataContext is not available, XYChart will wrap itself in a DataProvider and set this as the yScale config. */
yScale?: DataProviderProps<XScaleConfig, YScaleConfig>['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<Datum>) => 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<Datum>) => void;
/** Whether to invoke PointerEvent handlers for all dataKeys, or the nearest dataKey. */
pointerEventsDataKey?: 'all' | 'nearest';
};

const eventSourceSubscriptions = [XYCHART_EVENT_SOURCE];

export default function XYChart<
XScaleConfig extends ScaleConfig<AxisScaleOutput, any, any>,
YScaleConfig extends ScaleConfig<AxisScaleOutput, any, any>
>(props: XYChartProps<XScaleConfig, YScaleConfig>) {
YScaleConfig extends ScaleConfig<AxisScaleOutput, any, any>,
Datum extends object
>(props: XYChartProps<XScaleConfig, YScaleConfig, Datum>) {
const {
accessibilityLabel = 'XYChart',
captureEvents = true,
children,
height,
margin = DEFAULT_MARGIN,
onPointerMove,
onPointerOut,
onPointerUp,
pointerEventsDataKey = 'nearest',
theme,
width,
xScale,
Expand All @@ -64,12 +105,14 @@ 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,
]);
const pointerEventEmitters = usePointerEventEmitters({ source: XYCHART_EVENT_SOURCE });
usePointerEventHandlers({
dataKey: pointerEventsDataKey === 'nearest' ? POINTER_EVENTS_NEAREST : POINTER_EVENTS_ALL,
onPointerMove,
onPointerOut,
onPointerUp,
sources: eventSourceSubscriptions,
});

// if Context or dimensions are not available, wrap self in the needed providers
if (!setDimensions) {
Expand Down Expand Up @@ -121,16 +164,14 @@ export default function XYChart<
return width > 0 && height > 0 ? (
<svg width={width} height={height} aria-label={accessibilityLabel}>
{children}
{/** capture all pointer events and emit them. */}
{captureEvents && (
<rect
x={margin.left}
y={margin.top}
width={width - margin.left - margin.right}
height={height - margin.top - margin.bottom}
fill="transparent"
onPointerMove={handlePointerMove}
onPointerOut={handlePointerEnd}
{...pointerEventEmitters}
/>
)}
</svg>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ export default function AnimatedBarGroup<
XScale extends PositionScale,
YScale extends PositionScale,
Datum extends object
>({ ...props }: Omit<BaseBarGroupProps<XScale, YScale>, 'BarsComponent'>) {
>({ ...props }: Omit<BaseBarGroupProps<XScale, YScale, Datum>, 'BarsComponent'>) {
return <BaseBarGroup<XScale, YScale, Datum> {...props} BarsComponent={AnimatedBars} />;
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,8 @@ export default function AnimatedGlyphSeries<
renderGlyph?: React.FC<GlyphProps<Datum>>;
}) {
const renderGlyphs = useCallback(
({ glyphs, xScale, yScale, horizontal }: GlyphsProps<XScale, YScale, Datum>) => (
<AnimatedGlyphs
renderGlyph={renderGlyph}
glyphs={glyphs}
xScale={xScale}
yScale={yScale}
horizontal={horizontal}
/>
(glyphsProps: GlyphsProps<XScale, YScale, Datum>) => (
<AnimatedGlyphs {...glyphsProps} renderGlyph={renderGlyph} />
),
[renderGlyph],
);
Expand Down
2 changes: 1 addition & 1 deletion packages/visx-xychart/src/components/series/BarGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ export default function BarGroup<
XScale extends PositionScale,
YScale extends PositionScale,
Datum extends object
>({ ...props }: Omit<BaseBarGroupProps<XScale, YScale>, 'BarsComponent'>) {
>({ ...props }: Omit<BaseBarGroupProps<XScale, YScale, Datum>, 'BarsComponent'>) {
return <BaseBarGroup<XScale, YScale, Datum> {...props} BarsComponent={Bars} />;
}
8 changes: 6 additions & 2 deletions packages/visx-xychart/src/components/series/GlyphSeries.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,12 @@ export default function GlyphSeries<
renderGlyph?: React.FC<GlyphProps<Datum>>;
}) {
const renderGlyphs = useCallback(
({ glyphs }: GlyphsProps<XScale, YScale, Datum>) =>
glyphs.map(glyph => <React.Fragment key={glyph.key}>{renderGlyph(glyph)}</React.Fragment>),
({ glyphs, onPointerMove, onPointerOut, onPointerUp }: GlyphsProps<XScale, YScale, Datum>) =>
glyphs.map(glyph => (
<React.Fragment key={glyph.key}>
{renderGlyph({ ...glyph, onPointerMove, onPointerOut, onPointerUp })}
</React.Fragment>
)),
[renderGlyph],
);
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export default function AnimatedBars<XScale extends AxisScale, YScale extends Ax
item == null || key == null ? null : (
<animated.rect
key={key}
className="visx-bar"
x={x}
y={y}
width={width}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ export default function AnimatedGlyphs<
horizontal,
xScale,
yScale,
onPointerMove,
onPointerOut,
onPointerUp,
}: {
// unanimated Glyph component
renderGlyph: React.FC<GlyphProps<Datum>>;
Expand Down Expand Up @@ -87,6 +90,9 @@ export default function AnimatedGlyphs<
y: 0,
size: item.size,
color: 'currentColor', // allows us to animate the color of the <g /> element
onPointerMove,
onPointerOut,
onPointerUp,
})}
</animated.g>
))}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export default function AnimatedPath({
const tweened = useSpring({ stroke, fill });
return (
<animated.path
className="visx-path"
d={t.interpolate(interpolator)}
stroke={tweened.stroke}
fill={tweened.fill}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export default function Bars({
// eslint-disable-next-line react/jsx-no-useless-fragment
<>
{bars.map(({ key, ...barProps }) => (
<rect key={key} {...barProps} {...rectProps} />
<rect className="visx-bar" key={key} {...barProps} {...rectProps} />
))}
</>
);
Expand Down
Loading

0 comments on commit dd11a5c

Please sign in to comment.