Skip to content

Commit

Permalink
new(xychart): add AnimatedLineSeries + tests (#874)
Browse files Browse the repository at this point in the history
* internal(xychart): LineSeries => private/BaseLineSeries

* new(xychart): add AnimatedLineSeries, LineSeries, AnimatedPath

* new(demo/xychart): use AnimatedLineSeries

* test(xychart): add Animated(BarGroup, BarStack, BarSeries, LineSeries) tests

* fix(xychart/getScaledValueFactory): return NaN instead of null
  • Loading branch information
williaster authored Oct 22, 2020
1 parent 7514240 commit 48c4a34
Show file tree
Hide file tree
Showing 12 changed files with 211 additions and 84 deletions.
10 changes: 5 additions & 5 deletions packages/visx-demo/src/sandboxes/visx-xychart/Example.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import React from 'react';
import { CityTemperature } from '@visx/mock-data/lib/mocks/cityTemperature';
import {
AnimatedAxis,
AnimatedGrid,
DataProvider,
AnimatedBarGroup,
AnimatedBarSeries,
AnimatedBarStack,
LineSeries,
AnimatedGrid,
AnimatedLineSeries,
DataProvider,
Tooltip,
XYChart,
} from '@visx/xychart';
Expand Down Expand Up @@ -112,14 +112,14 @@ export default function Example({ height }: Props) {
)}
{renderLineSeries && (
<>
<LineSeries
<AnimatedLineSeries
dataKey="San Francisco"
data={renderBarStack ? data : data}
xAccessor={accessors.x['San Francisco']}
yAccessor={accessors.y['San Francisco']}
horizontal={!renderHorizontally}
/>
<LineSeries
<AnimatedLineSeries
dataKey="Austin"
data={renderBarStack ? data : data}
xAccessor={accessors.x.Austin}
Expand Down
12 changes: 12 additions & 0 deletions packages/visx-xychart/src/components/series/AnimatedLineSeries.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { AxisScale } from '@visx/axis';
import React from 'react';
import BaseLineSeries, { BaseLineSeriesProps } from './private/BaseLineSeries';
import AnimatedPath from './private/AnimatedPath';

export default function AnimatedLineSeries<
XScale extends AxisScale,
YScale extends AxisScale,
Datum extends object
>({ ...props }: Omit<BaseLineSeriesProps<XScale, YScale, Datum>, 'PathComponent'>) {
return <BaseLineSeries<XScale, YScale, Datum> {...props} PathComponent={AnimatedPath} />;
}
77 changes: 5 additions & 72 deletions packages/visx-xychart/src/components/series/LineSeries.tsx
Original file line number Diff line number Diff line change
@@ -1,78 +1,11 @@
import React, { useContext, useCallback } from 'react';
import LinePath from '@visx/shape/lib/shapes/LinePath';
import { AxisScale } from '@visx/axis';
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 findNearestDatumX from '../../utils/findNearestDatumX';
import TooltipContext from '../../context/TooltipContext';
import findNearestDatumY from '../../utils/findNearestDatumY';
import React from 'react';
import BaseLineSeries, { BaseLineSeriesProps } from './private/BaseLineSeries';

type LineSeriesProps<
export default function LineSeries<
XScale extends AxisScale,
YScale extends AxisScale,
Datum extends object
> = SeriesProps<XScale, YScale, Datum> & {
/** Whether line should be rendered horizontally instead of vertically. */
horizontal?: boolean;
};

function LineSeries<XScale extends AxisScale, YScale extends AxisScale, Datum extends object>({
data,
dataKey,
xAccessor,
xScale,
yAccessor,
yScale,
horizontal = true,
...lineProps
}: LineSeriesProps<XScale, YScale, Datum> & WithRegisteredDataProps<XScale, YScale, Datum>) {
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) => {
const { svgPoint } = params || {};
if (svgPoint && width && height && showTooltip) {
const datum = (horizontal ? findNearestDatumX : findNearestDatumY)({
point: svgPoint,
data,
xScale,
yScale,
xAccessor,
yAccessor,
width,
height,
});
if (datum) {
showTooltip({
key: dataKey,
...datum,
svgPoint,
});
}
}
},
[dataKey, data, xScale, yScale, xAccessor, yAccessor, width, height, showTooltip, horizontal],
);
useEventEmitter('mousemove', handleMouseMove);
useEventEmitter('mouseout', hideTooltip);

return (
<LinePath
data={data}
x={getScaledX}
y={getScaledY}
stroke={color}
strokeWidth={2}
{...lineProps}
/>
);
>({ ...props }: Omit<BaseLineSeriesProps<XScale, YScale, Datum>, 'PathComponent'>) {
return <BaseLineSeries<XScale, YScale, Datum> {...props} />;
}

export default withRegisteredData(LineSeries);
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import React from 'react';
import { animated, useSpring } from 'react-spring';

export default function AnimatedPath({
d,
stroke,
...lineProps
}: Omit<React.SVGProps<SVGPathElement>, 'ref'>) {
const tweened = useSpring({ d, stroke, config: { precision: 0.01 } });
return <animated.path d={tweened.d} stroke={tweened.stroke} fill="transparent" {...lineProps} />;
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export type BaseBarGroupProps<XScale extends PositionScale, YScale extends Posit
padding?: number;
/** Comparator function to sort `dataKeys` within a bar group. By default the DOM rendering order of `BarGroup`s `children` is used. */
sortBars?: (dataKeyA: string, dataKeyB: string) => number;
/** Rendered component which is passed BarsProps by BaseBarSeries after processing. */
/** Rendered component which is passed BarsProps by BaseBarGroup after processing. */
BarsComponent: React.FC<BarsProps<XScale, YScale>>;
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import React, { useContext, useCallback } from 'react';
import LinePath from '@visx/shape/lib/shapes/LinePath';
import { AxisScale } from '@visx/axis';
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 findNearestDatumX from '../../../utils/findNearestDatumX';
import TooltipContext from '../../../context/TooltipContext';
import findNearestDatumY from '../../../utils/findNearestDatumY';

export type BaseLineSeriesProps<
XScale extends AxisScale,
YScale extends AxisScale,
Datum extends object
> = SeriesProps<XScale, YScale, Datum> & {
/** Whether line should be rendered horizontally instead of vertically. */
horizontal?: boolean;
/** Rendered component which is passed path props by BaseLineSeries after processing. */
PathComponent?: React.FC<Omit<React.SVGProps<SVGPathElement>, 'ref'>> | 'path';
};

function BaseLineSeries<XScale extends AxisScale, YScale extends AxisScale, Datum extends object>({
data,
dataKey,
xAccessor,
xScale,
yAccessor,
yScale,
horizontal = true,
PathComponent = 'path',
...lineProps
}: BaseLineSeriesProps<XScale, YScale, Datum> & WithRegisteredDataProps<XScale, YScale, Datum>) {
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) => {
const { svgPoint } = params || {};
if (svgPoint && width && height && showTooltip) {
const datum = (horizontal ? findNearestDatumX : findNearestDatumY)({
point: svgPoint,
data,
xScale,
yScale,
xAccessor,
yAccessor,
width,
height,
});
if (datum) {
showTooltip({
key: dataKey,
...datum,
svgPoint,
});
}
}
},
[dataKey, data, xScale, yScale, xAccessor, yAccessor, width, height, showTooltip, horizontal],
);
useEventEmitter('mousemove', handleMouseMove);
useEventEmitter('mouseout', hideTooltip);

return (
<LinePath
data={data}
x={getScaledX}
y={getScaledY}
stroke={color}
strokeWidth={2}
{...lineProps}
>
{({ path }) => (
<PathComponent stroke={color} strokeWidth={2} {...lineProps} d={path(data) || ''} />
)}
</LinePath>
);
}

export default withRegisteredData(BaseLineSeries);
1 change: 1 addition & 0 deletions packages/visx-xychart/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export { default as LineSeries } from './components/series/LineSeries';
export { default as AnimatedBarSeries } from './components/series/AnimatedBarSeries';
export { default as AnimatedBarStack } from './components/series/AnimatedBarStack';
export { default as AnimatedBarGroup } from './components/series/AnimatedBarGroup';
export { default as AnimatedLineSeries } from './components/series/AnimatedLineSeries';

// context
export { default as DataContext } from './context/DataContext';
Expand Down
4 changes: 4 additions & 0 deletions packages/visx-xychart/src/utils/getScaledValueFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ export default function getScaledValueFactory<Scale extends AxisScale, Datum>(
(align === 'start' ? 0 : getScaleBandwidth(scale)) / (align === 'center' ? 2 : 1);
return scaledValue + bandwidthOffset;
}
// @TODO: NaNs cause react-spring to throw, but the return value of this must be number
// this currently causes issues in vertical <> horizontal transitions because
// x/yAccessors from context are out of sync with props.horizontal
// horizontal should be moved to context
return NaN;
};
}
24 changes: 22 additions & 2 deletions packages/visx-xychart/test/components/BarGroup.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { useEffect } from 'react';
import { animated } from 'react-spring';
import { mount } from 'enzyme';
import { BarGroup, BarSeries, DataProvider, useEventEmitter } from '../../src';
import { AnimatedBarGroup, BarGroup, BarSeries, DataProvider, useEventEmitter } from '../../src';
import setupTooltipTest from '../mocks/setupTooltipTest';

const providerProps = {
Expand Down Expand Up @@ -77,7 +78,7 @@ describe('<BarGroup />', () => {

setupTooltipTest(
<>
<BarGroup horizontal>
<BarGroup>
<BarSeries dataKey={series1.key} {...series1} />
<BarSeries dataKey={series2.key} {...series2} />
</BarGroup>
Expand All @@ -87,3 +88,22 @@ describe('<BarGroup />', () => {
);
});
});

describe('<AnimatedBarGroup />', () => {
it('should be defined', () => {
expect(AnimatedBarGroup).toBeDefined();
});
it('should render an animated.rect', () => {
const wrapper = mount(
<DataProvider {...providerProps}>
<svg>
<AnimatedBarGroup>
<BarSeries dataKey={series1.key} {...series1} />
<BarSeries dataKey={series2.key} {...series2} />
</AnimatedBarGroup>
</svg>
</DataProvider>,
);
expect(wrapper.find(animated.rect)).toHaveLength(4);
});
});
19 changes: 18 additions & 1 deletion packages/visx-xychart/test/components/BarSeries.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { useContext, useEffect } from 'react';
import { animated } from 'react-spring';
import { mount } from 'enzyme';
import { DataContext, BarSeries, useEventEmitter } from '../../src';
import { DataContext, AnimatedBarSeries, BarSeries, useEventEmitter } from '../../src';
import getDataContext from '../mocks/getDataContext';
import setupTooltipTest from '../mocks/setupTooltipTest';

Expand Down Expand Up @@ -62,3 +63,19 @@ describe('<BarSeries />', () => {
);
});
});

describe('<AnimatedBarSeries />', () => {
it('should be defined', () => {
expect(AnimatedBarSeries).toBeDefined();
});
it('should render an animated.rect', () => {
const wrapper = mount(
<DataContext.Provider value={getDataContext(series)}>
<svg>
<AnimatedBarSeries dataKey={series.key} {...series} />
</svg>
</DataContext.Provider>,
);
expect(wrapper.find(animated.rect)).toHaveLength(series.data.length);
});
});
31 changes: 29 additions & 2 deletions packages/visx-xychart/test/components/BarStack.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import React, { useContext, useEffect } from 'react';
import { mount } from 'enzyme';
import { BarStack, BarSeries, DataProvider, DataContext, useEventEmitter } from '../../src';
import { animated } from 'react-spring';
import {
BarStack,
BarSeries,
DataProvider,
DataContext,
useEventEmitter,
AnimatedBarStack,
} from '../../src';
import setupTooltipTest from '../mocks/setupTooltipTest';

const providerProps = {
Expand Down Expand Up @@ -108,7 +116,7 @@ describe('<BarStack />', () => {

setupTooltipTest(
<>
<BarStack horizontal>
<BarStack>
<BarSeries dataKey={series1.key} {...series1} />
<BarSeries dataKey={series2.key} {...series2} />
</BarStack>
Expand All @@ -118,3 +126,22 @@ describe('<BarStack />', () => {
);
});
});

describe('<AnimatedBarStack />', () => {
it('should be defined', () => {
expect(AnimatedBarStack).toBeDefined();
});
it('should render an animated.rect', () => {
const wrapper = mount(
<DataProvider {...providerProps}>
<svg>
<AnimatedBarStack>
<BarSeries dataKey={series1.key} {...series1} />
<BarSeries dataKey={series2.key} {...series2} />
</AnimatedBarStack>
</svg>
</DataProvider>,
);
expect(wrapper.find(animated.rect)).toHaveLength(4);
});
});
Loading

0 comments on commit 48c4a34

Please sign in to comment.