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

test(xychart): add AreaStack tests #1036

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 7 additions & 3 deletions packages/visx-xychart/src/components/axis/BaseAxis.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,11 @@ export default function BaseAxis<Scale extends AxisScale>({
? (width ?? 0) - (margin?.right ?? 0)
: 0;

return (
const scale = (orientation === 'left' || orientation === 'right' ? yScale : xScale) as
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this look cleaner?
const scale: Scale | undefined = ...

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely ideal, but the type won't work without a coercion either here, or when I get the scales from DataContext (I haven't successfully figured out how to use generics with Context types; most Series coerce the Context values right now)

| Scale
| undefined;

return scale ? (
<AxisComponent
top={topOffset}
left={leftOffset}
Expand All @@ -77,7 +81,7 @@ export default function BaseAxis<Scale extends AxisScale>({
tickStroke={axisStyles?.tickLine?.stroke}
{...props}
tickLabelProps={tickLabelProps}
scale={(orientation === 'left' || orientation === 'right' ? yScale : xScale) as Scale}
scale={scale}
/>
);
) : null;
}
11 changes: 6 additions & 5 deletions packages/visx-xychart/src/hooks/useDimensions.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
import { useCallback, useState } from 'react';

const INITIAL_DIMENSIONS = {
const DEFAULT_DIMS = {
width: 0,
height: 0,
margin: { top: 0, right: 0, bottom: 0, left: 0 },
};

export type Dimensions = typeof INITIAL_DIMENSIONS;
export type Dimensions = typeof DEFAULT_DIMS;

/** A hook for accessing and setting memoized width, height, and margin chart dimensions. */
export default function useDimensions(
initialDimensions?: Partial<Dimensions>,
initialDims?: Partial<Dimensions>,
): [Dimensions, (dims: Dimensions) => void] {
const [dimensions, privateSetDimensions] = useState<Dimensions>({
...INITIAL_DIMENSIONS,
...initialDimensions,
width: initialDims?.width == null ? DEFAULT_DIMS.width : initialDims.width,
height: initialDims?.height == null ? DEFAULT_DIMS.height : initialDims.height,
margin: initialDims?.margin == null ? DEFAULT_DIMS.margin : initialDims.margin,
});

// expose a setter with better memoization logic
Expand Down
10 changes: 8 additions & 2 deletions packages/visx-xychart/src/hooks/useScales.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ export default function useScales<
[],
);

// d3Extent scale returns NaN domain for empty arrays
if (xValues.length === 0) return undefined;

const xDomain = isDiscreteScale(xScaleConfig) ? xValues : d3Extent(xValues);

let xScale = (scaleCanBeZeroed(xScaleConfig)
Expand All @@ -54,7 +57,7 @@ export default function useScales<
...xScaleConfig,
})) as XScale;

// apply any scale updates from the registy
// apply any scale updates from the registry
registryEntries.forEach(entry => {
if (entry?.xScale) xScale = entry.xScale(xScale);
});
Expand All @@ -74,6 +77,9 @@ export default function useScales<
[],
);

// d3Extent scale returns NaN domain for empty arrays
if (yValues.length === 0) return undefined;

const yDomain = isDiscreteScale(yScaleConfig) ? yValues : d3Extent(yValues);

let yScale = (scaleCanBeZeroed(yScaleConfig)
Expand All @@ -89,7 +95,7 @@ export default function useScales<
...yScaleConfig,
})) as YScale;

// apply any scale updates from the registy
// apply any scale updates from the registry
registryEntries.forEach(entry => {
if (entry?.yScale) yScale = entry.yScale(yScale);
});
Expand Down
12 changes: 6 additions & 6 deletions packages/visx-xychart/src/providers/DataProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,20 +48,20 @@ export default function DataProvider<
const contextTheme = useContext(ThemeContext);
const theme = propsTheme || contextTheme;
const [{ width, height, margin }, setDimensions] = useDimensions(initialDimensions);
const innerWidth = width - (margin?.left ?? 0) - (margin?.right ?? 0);
const innerHeight = height - (margin?.top ?? 0) - (margin?.bottom ?? 0);
const innerWidth = Math.max(0, width - margin.left - margin.right);
const innerHeight = Math.max(0, height - margin.top - margin.bottom);

type XScale = ScaleConfigToD3Scale<XScaleConfig, AxisScaleOutput, any, any>;
type YScale = ScaleConfigToD3Scale<YScaleConfig, AxisScaleOutput, any, any>;

const dataRegistry = useDataRegistry<XScale, YScale, Datum>();

const { xScale, yScale }: { xScale: XScale; yScale: YScale } = useScales({
const { xScale, yScale }: { xScale?: XScale; yScale?: YScale } = useScales({
dataRegistry,
xScaleConfig,
yScaleConfig,
xRange: [margin.left, width - margin.right],
yRange: [height - margin.bottom, margin.top],
xRange: [margin.left, Math.max(0, width - margin.right)],
yRange: [Math.max(0, height - margin.bottom), margin.top],
});

const registryKeys = dataRegistry.keys();
Expand All @@ -82,7 +82,7 @@ export default function DataProvider<

return (
<DataContext.Provider
// everthing returned here should be memoized between renders
// everything returned here should be memoized between renders
// to avoid child re-renders
value={{
dataRegistry,
Expand Down
5 changes: 3 additions & 2 deletions packages/visx-xychart/src/utils/combineBarStackData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ export const getStackValue = <XScale extends AxisScale, YScale extends AxisScale
) => d.stack;

/**
* Merges `seriesChildren` `data` by their `stack` value which forms the stack grouping
* (`x` if vertical, `y` if horizontal) and returns `CombinedStackData[]`.
* Merges `seriesChildren` `props.data` by their `stack` value which
* forms the stack grouping (`x` if vertical, `y` if horizontal)
* and returns `CombinedStackData[]`.
*/
export default function combineBarStackData<
XScale extends AxisScale,
Expand Down
4 changes: 2 additions & 2 deletions packages/visx-xychart/src/utils/getScaleBandwidth.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { AxisScale } from '@visx/axis';

export default function getScaleBandwidth<Scale extends AxisScale>(scale: Scale) {
export default function getScaleBandwidth<Scale extends AxisScale>(scale?: Scale) {
// Broaden type before using 'xxx' in s as typeguard.
const s = scale as AxisScale;
return 'bandwidth' in s ? s?.bandwidth() ?? 0 : 0;
return s && 'bandwidth' in s ? s?.bandwidth() ?? 0 : 0;
}
180 changes: 180 additions & 0 deletions packages/visx-xychart/test/components/AreaStack.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import React, { useContext, useEffect } from 'react';
import { mount } from 'enzyme';
import { animated } from 'react-spring';
import { Area, LinePath } from '@visx/shape';
import {
AreaStack,
AreaSeries,
DataProvider,
DataContext,
useEventEmitter,
AnimatedAreaStack,
} from '../../src';
import setupTooltipTest from '../mocks/setupTooltipTest';
import { XYCHART_EVENT_SOURCE } from '../../src/constants';

const providerProps = {
initialDimensions: { width: 100, height: 100 },
xScale: { type: 'linear' },
yScale: { type: 'linear' },
} as const;

const accessors = {
xAccessor: (d: { x?: number }) => d.x,
yAccessor: (d: { y?: number }) => d.y,
};

const series1 = {
key: 'area1',
data: [
{ x: 10, y: 5 },
{ x: 7, y: 5 },
],
...accessors,
};

const series2 = {
key: 'area2',
data: [
{ x: 10, y: 5 },
{ x: 7, y: 20 },
],
...accessors,
};

describe('<AreaStack />', () => {
it('should be defined', () => {
expect(AreaSeries).toBeDefined();
});

it('should render Areas', () => {
const wrapper = mount(
<DataProvider {...providerProps}>
<svg>
<AreaStack>
<AreaSeries dataKey={series1.key} {...series1} />
<AreaSeries dataKey={series2.key} {...series2} />
</AreaStack>
</svg>
</DataProvider>,
);
// @ts-ignore produces a union type that is too complex to represent.ts(2590)
expect(wrapper.find(Area)).toHaveLength(2);
});

it('should render LinePaths if renderLine=true', () => {
const wrapper = mount(
<DataProvider {...providerProps}>
<svg>
<AreaStack renderLine>
<AreaSeries dataKey={series1.key} {...series1} />
<AreaSeries dataKey={series2.key} {...series2} />
</AreaStack>
</svg>
</DataProvider>,
);
// @ts-ignore produces a union type that is too complex to represent.ts(2590)
expect(wrapper.find(LinePath)).toHaveLength(2);
});

it('should render Glyphs if focus/blur handlers are set', () => {
const wrapper = mount(
<DataProvider {...providerProps}>
<svg>
<AreaStack onFocus={() => {}}>
<AreaSeries dataKey={series1.key} {...series1} />
</AreaStack>
</svg>
</DataProvider>,
);
expect(wrapper.find('circle')).toHaveLength(series1.data.length);
});

it('should update scale domain to include stack sums including negative values', () => {
expect.hasAssertions();

function Assertion() {
const { yScale } = useContext(DataContext);
if (yScale) {
expect(yScale.domain()).toEqual([-20, 10]);
}
return null;
}

mount(
<DataProvider {...providerProps}>
<svg>
<AreaStack>
<AreaSeries dataKey={series1.key} {...series1} />
<AreaSeries
dataKey={series2.key}
{...series2}
data={[
{ x: 10, y: 5 },
{ x: 7, y: -20 },
]}
/>
</AreaStack>
</svg>
<Assertion />
</DataProvider>,
);
});

it('should invoke showTooltip/hideTooltip on pointermove/pointerout', () => {
expect.assertions(2);

const showTooltip = jest.fn();
const hideTooltip = jest.fn();

const EventEmitter = () => {
const emit = useEventEmitter();
const { yScale } = useContext(DataContext);

useEffect(() => {
// checking for yScale ensures stack data is registered and stacks are rendered
if (emit && yScale) {
// @ts-ignore not a React.MouseEvent
emit('pointermove', new MouseEvent('pointermove'), XYCHART_EVENT_SOURCE);
expect(showTooltip).toHaveBeenCalledTimes(2); // one per key

// @ts-ignore not a React.MouseEvent
emit('pointerout', new MouseEvent('pointerout'), XYCHART_EVENT_SOURCE);
expect(showTooltip).toHaveBeenCalled();
}
});

return null;
};

setupTooltipTest(
<>
<AreaStack>
<AreaSeries dataKey={series1.key} {...series1} />
<AreaSeries dataKey={series2.key} {...series2} />
</AreaStack>
<EventEmitter />
</>,
{ showTooltip, hideTooltip },
);
});
});

describe('<AnimatedAreaStack />', () => {
it('should be defined', () => {
expect(AnimatedAreaStack).toBeDefined();
});
it('should render an animated.path', () => {
const wrapper = mount(
<DataProvider {...providerProps}>
<svg>
<AnimatedAreaStack renderLine={false}>
<AreaSeries dataKey={series1.key} {...series1} />
<AreaSeries dataKey={series2.key} {...series2} />
</AnimatedAreaStack>
</svg>
</DataProvider>,
);
expect(wrapper.find(animated.path)).toHaveLength(2);
});
});
Loading