Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

new(xychart): add PointerEvent handlers to XYChart and *Series #947

Merged
merged 24 commits into from
Dec 3, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
119c48c
new(xychart): add event source constants
williaster Nov 25, 2020
754b523
new(xychart): add hooks/usePointerEventEmitters
williaster Nov 25, 2020
9b8ebfd
new(xychart): add hooks/usePointerEventHandlers
williaster Nov 25, 2020
be54889
new(xychart/useEventEmitter): update to pointer events, add prop anno…
williaster Nov 25, 2020
12509aa
internal(xychart/findNearestDatum): factor out return type to types
williaster Nov 25, 2020
16e247f
types(xychart): add pointer handlers to series types
williaster Nov 25, 2020
c4c55ce
new(xychart/XYChart): add pointer handlers, refactor to usePointerEve…
williaster Nov 25, 2020
3cc437a
new(xychart/BaseLineSeries): add pointer handlers, refactor to usePoi…
williaster Nov 25, 2020
6120dab
new(xychart/BaseAreaSeries): add pointer handlers, refactor to usePoi…
williaster Nov 25, 2020
692ebcc
new(xychart/BaseGlyphSeries): add pointer handlers, refactor to usePo…
williaster Nov 25, 2020
5cfd82b
new(xychart/BaseBarSeries): add pointer handlers, refactor to usePoin…
williaster Nov 25, 2020
5e23397
neww(xychart/BaseBarGroup): add pointer handlers, refactor to usePoin…
williaster Nov 25, 2020
1127da4
neww(xychart/BaseBarStack): add pointer handlers, refactor to usePoin…
williaster Nov 25, 2020
01be760
new(xychart/AnimatedPath): add className
williaster Nov 25, 2020
041f37e
new(demo/xychart): add onPointerUp example to demo
williaster Nov 25, 2020
1750d44
fix(xychart): consider pointerEvents for Series pointer event emitter…
williaster Nov 25, 2020
c7db4be
fix(demo/xychart): add setAnnotationDataIndex, setAnnotationDataKey t…
williaster Nov 25, 2020
59d41f8
internal(xychart): move useEventEmitter calls to usePointerEventHandl…
williaster Nov 25, 2020
fe5cfbe
api(xychart/XYChart): rename prop pointerEvents => pointerEventsDataKey
williaster Nov 25, 2020
bbab46b
internal(xychart/usePointerEventHandlers): fix comment typo
williaster Nov 25, 2020
0cbdc42
fix(xychart/useEventEmitter): mousemove => pointermove
williaster Nov 26, 2020
e6698ef
test(xychart): update mousemove/out => pointermove/out
williaster Dec 1, 2020
ea173fb
type(xychart): fix types
williaster Dec 1, 2020
dda5427
test(xychart): add event source to fix Series tests
williaster Dec 1, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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