Skip to content

Commit

Permalink
Merge 55e440f into 7ce551f
Browse files Browse the repository at this point in the history
  • Loading branch information
williaster committed Oct 1, 2020
2 parents 7ce551f + 55e440f commit 3882dee
Show file tree
Hide file tree
Showing 21 changed files with 512 additions and 18 deletions.
4 changes: 4 additions & 0 deletions packages/visx-demo/src/sandboxes/visx-xychart/Example.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
DataProvider,
BarSeries,
LineSeries,
Tooltip,
XYChart,
} from '@visx/xychart';
import ExampleControls from './ExampleControls';
Expand Down Expand Up @@ -82,6 +83,9 @@ export default function Example({ height }: Props) {
numTicks={numTicks}
animationTrajectory={animationTrajectory}
/>
<Tooltip
renderTooltip={({ tooltipData }) => <pre>{JSON.stringify(tooltipData, null, 2)}</pre>}
/>
</XYChart>
</DataProvider>
)}
Expand Down
4 changes: 4 additions & 0 deletions packages/visx-xychart/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,18 @@
"@types/classnames": "^2.2.9",
"@types/react": "*",
"@visx/axis": "1.0.0",
"@visx/event": "1.0.0",
"@visx/grid": "1.0.0",
"@visx/react-spring": "1.0.0",
"@visx/responsive": "1.0.0",
"@visx/scale": "1.0.0",
"@visx/shape": "1.0.0",
"@visx/text": "1.0.0",
"@visx/tooltip": "1.0.0",
"@visx/voronoi": "1.0.0",
"classnames": "^2.2.5",
"d3-array": "2.6.0",
"mitt": "^2.1.0",
"prop-types": "^15.6.2"
}
}
76 changes: 76 additions & 0 deletions packages/visx-xychart/src/components/Tooltip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import React, { useCallback, useContext } from 'react';
import { useTooltipInPortal, defaultStyles } from '@visx/tooltip';
import { TooltipProps as BaseTooltipProps } from '@visx/tooltip/lib/tooltips/Tooltip';
import { PickD3Scale } from '@visx/scale';

import TooltipContext from '../context/TooltipContext';
import DataContext from '../context/DataContext';
import { TooltipContextType } from '../types';

export type RenderTooltipParams = TooltipContextType & {
colorScale?: PickD3Scale<'ordinal', string, string>;
};

export type TooltipProps = {
/**
* When TooltipContext.tooltipOpen=true, this function is invoked and if the
* return value is non-null, its content is rendered inside the tooltip container.
* Content will be rendered in an HTML parent.
*/
renderTooltip: (params: RenderTooltipParams) => React.ReactNode;
} & Omit<BaseTooltipProps, 'left' | 'top' | 'children'>;

const INVISIBLE_STYLES: React.CSSProperties = {
position: 'absolute',
left: 0,
top: 0,
opacity: 0,
width: 0,
height: 0,
pointerEvents: 'none',
};

export default function Tooltip({ renderTooltip, ...tooltipProps }: TooltipProps) {
const { colorScale, theme } = useContext(DataContext) || {};
const tooltipContext = useContext(TooltipContext);
const { containerRef, TooltipInPortal } = useTooltipInPortal();

// To correctly position itself in a Portal, the tooltip must know its container bounds
// this is done by rendering an invisible node which can be used to find its parents element
const setContainerRef = useCallback(
(ownRef: HTMLElement | SVGElement | null) => {
containerRef(ownRef?.parentElement ?? null);
},
[containerRef],
);

const tooltipContent = tooltipContext?.tooltipOpen
? renderTooltip({ ...tooltipContext, colorScale })
: null;

return (
// Tooltip can be rendered as a child of SVG or HTML since its output is rendered in a Portal.
// So use svg element to find container ref because it's a valid child of SVG and HTML parents.
<>
<svg ref={setContainerRef} style={INVISIBLE_STYLES} />
{tooltipContext?.tooltipOpen && tooltipContent != null && (
<TooltipInPortal
left={tooltipContext?.tooltipLeft}
top={tooltipContext?.tooltipTop}
style={{
...defaultStyles,
background: theme?.backgroundColor ?? 'white',
boxShadow: `0 1px 2px ${
theme?.htmlLabelStyles?.color ? `${theme?.htmlLabelStyles?.color}55` : '#22222255'
}`,

...theme?.htmlLabelStyles,
}}
{...tooltipProps}
>
{tooltipContent}
</TooltipInPortal>
)}
</>
);
}
57 changes: 53 additions & 4 deletions packages/visx-xychart/src/components/XYChart.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import React, { useContext, useEffect } from 'react';
/* eslint-disable jsx-a11y/mouse-events-have-key-events */
import React, { useCallback, useContext, useEffect } from 'react';
import ParentSize from '@visx/responsive/lib/components/ParentSize';

import DataContext from '../context/DataContext';
import { Margin } from '../types';
import useEventEmitter from '../hooks/useEventEmitter';
import EventEmitterProvider from '../providers/EventEmitterProvider';
import TooltipContext from '../context/TooltipContext';
import TooltipProvider from '../providers/TooltipProvider';

const DEFAULT_MARGIN = { top: 50, right: 50, bottom: 50, left: 50 };

type Props = {
accessibilityLabel?: string;
events?: boolean;
width?: number;
height?: number;
Expand All @@ -15,8 +21,16 @@ type Props = {
};

export default function XYChart(props: Props) {
const { children, width, height, margin = DEFAULT_MARGIN } = props;
const {
accessibilityLabel = 'XYChart',
children,
width,
height,
margin = DEFAULT_MARGIN,
} = props;
const { setDimensions } = useContext(DataContext);
const tooltipContext = useContext(TooltipContext);
const emit = useEventEmitter();

// update dimensions in context
useEffect(() => {
Expand All @@ -25,14 +39,49 @@ export default function XYChart(props: Props) {
}
}, [setDimensions, width, height, margin]);

// if width and height aren't both provided, wrap in auto-sizer + preserve passed dims
const handleMouseTouchMove = useCallback(
(event: React.MouseEvent | React.TouchEvent) => emit?.('mousemove', event),
[emit],
);
const handleMouseOutTouchEnd = useCallback(
(event: React.MouseEvent | React.TouchEvent) => emit?.('mouseout', event),
[emit],
);

// if Context or dimensions are not available, wrap self in the needed providers
if (width == null || height == null) {
return <ParentSize>{dims => <XYChart {...dims} {...props} />}</ParentSize>;
}
if (emit == null) {
return (
<EventEmitterProvider>
<XYChart {...props} />
</EventEmitterProvider>
);
}
if (tooltipContext == null) {
return (
<TooltipProvider>
<XYChart {...props} />
</TooltipProvider>
);
}

return width > 0 && height > 0 ? (
<svg width={width} height={height}>
<svg width={width} height={height} aria-label={accessibilityLabel}>
{children}
{/** capture all mouse/touch events and emit them. */}
<rect
x={margin.left}
y={margin.top}
width={width - margin.left - margin.right}
height={height - margin.top - margin.bottom}
fill="transparent"
onMouseMove={handleMouseTouchMove}
onTouchMove={handleMouseTouchMove}
onMouseOut={handleMouseOutTouchEnd}
onTouchEnd={handleMouseOutTouchEnd}
/>
</svg>
) : null;
}
43 changes: 40 additions & 3 deletions packages/visx-xychart/src/components/series/BarSeries.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import withRegisteredData, { WithRegisteredDataProps } from '../../enhancers/wit
import getScaledValueFactory from '../../utils/getScaledValueFactory';
import isValidNumber from '../../typeguards/isValidNumber';
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';

type BarSeriesProps<
XScale extends AxisScale,
Expand Down Expand Up @@ -36,7 +40,9 @@ function BarSeries<XScale extends AxisScale, YScale extends AxisScale, Datum ext
yAccessor,
yScale,
}: BarSeriesProps<XScale, YScale, Datum> & WithRegisteredDataProps<XScale, YScale, Datum>) {
const { colorScale, theme, innerWidth = 0, innerHeight = 0 } = useContext(DataContext);
const { colorScale, theme, width, height, innerWidth = 0, innerHeight = 0 } = useContext(
DataContext,
);
const getScaledX = useCallback(getScaledValueFactory(xScale, xAccessor), [xScale, xAccessor]);
const getScaledY = useCallback(getScaledValueFactory(yScale, yAccessor), [yScale, yAccessor]);
const [xMin, xMax] = xScale.range().map(Number);
Expand Down Expand Up @@ -80,10 +86,41 @@ function BarSeries<XScale extends AxisScale, YScale extends AxisScale, Datum ext
});
}, [barThickness, color, data, getScaledX, getScaledY, horizontal, xZeroPosition, yZeroPosition]);

const { showTooltip, hideTooltip } = useContext(TooltipContext) ?? {};
const handleMouseMove = useCallback(
(params: HandlerParams | undefined) => {
const { event, svgCoords } = params || {};
if (event && svgCoords && width && height && showTooltip) {
const datum = (horizontal ? findNearestDatumY : findNearestDatumX)({
event,
svgCoords,
key: dataKey,
data,
xScale,
yScale,
xAccessor,
yAccessor,
width,
height,
});
if (datum) {
showTooltip({
tooltipData: datum.datum,
tooltipLeft: svgCoords.x,
tooltipTop: svgCoords.y,
});
}
}
},
[dataKey, data, horizontal, xScale, yScale, xAccessor, yAccessor, width, height, showTooltip],
);
useEventEmitter('mousemove', handleMouseMove);
useEventEmitter('mouseout', hideTooltip);

return (
<g className="vx-bar-series">
{bars.map(({ x, y, width, height, fill }, i) => (
<rect key={i} x={x} y={y} width={width} height={height} fill={fill} />
{bars.map(({ x, y, width: barWidth, height: barHeight, fill }, i) => (
<rect key={i} x={x} y={y} width={barWidth} height={barHeight} fill={fill} />
))}
</g>
);
Expand Down
36 changes: 35 additions & 1 deletion packages/visx-xychart/src/components/series/LineSeries.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import DataContext from '../../context/DataContext';
import { SeriesProps } from '../../types';
import withRegisteredData, { WithRegisteredDataProps } from '../../enhancers/withRegisteredData';
import getScaledValueFactory from '../../utils/getScaledValueFactory';
import useEventEmitter, { HandlerParams } from '../../hooks/useEventEmitter';
import findNearestDatumXY from '../../utils/findNearestDatumXY';
import TooltipContext from '../../context/TooltipContext';

type LineSeriesProps<
XScale extends AxisScale,
Expand All @@ -21,11 +24,42 @@ function LineSeries<XScale extends AxisScale, YScale extends AxisScale, Datum ex
yScale,
...lineProps
}: LineSeriesProps<XScale, YScale, Datum> & WithRegisteredDataProps<XScale, YScale, Datum>) {
const { colorScale, theme } = useContext(DataContext);
const { colorScale, theme, width, height } = useContext(DataContext);
const { showTooltip, hideTooltip } = useContext(TooltipContext) ?? {};
const getScaledX = useCallback(getScaledValueFactory(xScale, xAccessor), [xScale, xAccessor]);
const getScaledY = useCallback(getScaledValueFactory(yScale, yAccessor), [yScale, yAccessor]);
const color = colorScale?.(dataKey) ?? theme?.colors?.[0] ?? '#222';

const handleMouseMove = useCallback(
(params: HandlerParams | undefined) => {
const { event, svgCoords } = params || {};
if (event && svgCoords && width && height && showTooltip) {
const datum = findNearestDatumXY({
event,
svgCoords,
key: dataKey,
data,
xScale,
yScale,
xAccessor,
yAccessor,
width,
height,
});
if (datum) {
showTooltip({
tooltipData: datum.datum,
tooltipLeft: svgCoords.x,
tooltipTop: svgCoords.y,
});
}
}
},
[dataKey, data, xScale, yScale, xAccessor, yAccessor, width, height, showTooltip],
);
useEventEmitter('mousemove', handleMouseMove);
useEventEmitter('mouseout', hideTooltip);

return (
<LinePath
data={data}
Expand Down
6 changes: 6 additions & 0 deletions packages/visx-xychart/src/context/EventEmitterContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { createContext } from 'react';
import { EventEmitterContextType } from '../types';

const EventEmitterContext = createContext<EventEmitterContextType | null>(null);

export default EventEmitterContext;
6 changes: 6 additions & 0 deletions packages/visx-xychart/src/context/TooltipContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { createContext } from 'react';
import { TooltipContextType } from '../types';

const TooltipContext = createContext<TooltipContextType | null>(null);

export default TooltipContext;
35 changes: 35 additions & 0 deletions packages/visx-xychart/src/hooks/useEventEmitter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
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 HandlerParams = {
event: React.MouseEvent | React.TouchEvent;
svgCoords: ReturnType<typeof localPoint>;
};
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) {
const emitter = useContext(EventEmitterContext);

/** wrap emitter.emit so we can enforce stricter type signature */
const emit = useCallback(
(type: EventType, event: HandlerParams['event']) =>
emitter?.emit<HandlerParams>(type, { event, svgCoords: localPoint(event) }),
[emitter],
);

useEffect(() => {
if (emitter && eventType && handler) {
emitter.on<HandlerParams>(eventType, handler);
return () => emitter?.off<HandlerParams>(eventType, handler);
}
return undefined;
}, [emitter, eventType, handler]);

return emitter ? emit : null;
}

0 comments on commit 3882dee

Please sign in to comment.