diff --git a/packages/visx-xychart/src/components/XYChart.tsx b/packages/visx-xychart/src/components/XYChart.tsx index bee690d1f..d3f42b235 100644 --- a/packages/visx-xychart/src/components/XYChart.tsx +++ b/packages/visx-xychart/src/components/XYChart.tsx @@ -1,11 +1,12 @@ /* eslint jsx-a11y/mouse-events-have-key-events: 'off', @typescript-eslint/no-explicit-any: 'off' */ import React, { useContext, useEffect } from 'react'; import ParentSize from '@visx/responsive/lib/components/ParentSize'; -import { AxisScaleOutput } from '@visx/axis'; +import { AxisScale, AxisScaleOutput } from '@visx/axis'; import { ScaleConfig } from '@visx/scale'; import DataContext from '../context/DataContext'; import { Margin, EventHandlerParams } from '../types'; +import { DataRegistryEntry } from '../types/data'; import useEventEmitter from '../hooks/useEventEmitter'; import EventEmitterProvider from '../providers/EventEmitterProvider'; import TooltipContext from '../context/TooltipContext'; @@ -35,14 +36,18 @@ export type XYChartProps< height?: number; /** Margin to apply around the outside. */ margin?: Margin; + /** XYChart data to be rendered in Series. */ + data?: + | DataRegistryEntry + | DataRegistryEntry[]; /** XYChart children (Series, Tooltip, etc.). */ children: React.ReactNode; /** If DataContext is not available, XYChart will wrap itself in a DataProvider and set this as the theme. */ - theme?: DataProviderProps['theme']; + theme?: DataProviderProps['theme']; /** If DataContext is not available, XYChart will wrap itself in a DataProvider and set this as the xScale config. */ - xScale?: DataProviderProps['xScale']; + xScale?: DataProviderProps['xScale']; /** If DataContext is not available, XYChart will wrap itself in a DataProvider and set this as the yScale config. */ - yScale?: DataProviderProps['yScale']; + yScale?: DataProviderProps['yScale']; /* If DataContext is not available, XYChart will wrap itself in a DataProvider and set this as horizontal. Determines whether Series will be plotted horizontally (e.g., horizontal bars). By default this will try to be inferred based on scale types. */ horizontal?: boolean | 'auto'; /** Callback invoked for onPointerMove events for the nearest Datum to the PointerEvent _for each Series with pointerEvents={true}_. */ @@ -85,6 +90,7 @@ export default function XYChart< accessibilityLabel = 'XYChart', captureEvents = true, children, + data, height, horizontal, margin = DEFAULT_MARGIN, @@ -127,6 +133,7 @@ export default function XYChart< } return ( & WithRegisteredDataProps) { - const { colorScale, theme, horizontal } = useContext(DataContext); + const { colorScale, dataRegistry, theme, horizontal } = useContext(DataContext); + + const xAccessor: (d: Datum) => ScaleInput = _xAccessor ?? dataRegistry.get(dataKey).xAccessor; + const yAccessor: (d: Datum) => ScaleInput = _yAccessor ?? dataRegistry.get(dataKey).yAccessor; const getScaledX0 = useMemo( () => (x0Accessor ? getScaledValueFactory(xScale, x0Accessor) : undefined), [xScale, x0Accessor], diff --git a/packages/visx-xychart/src/components/series/private/BaseAreaStack.tsx b/packages/visx-xychart/src/components/series/private/BaseAreaStack.tsx index d297ef664..0ed40a780 100644 --- a/packages/visx-xychart/src/components/series/private/BaseAreaStack.tsx +++ b/packages/visx-xychart/src/components/series/private/BaseAreaStack.tsx @@ -147,7 +147,7 @@ function BaseAreaStack): NearestDatumReturnType => { const childData = seriesChildren.find((child) => child.props.dataKey === params.dataKey) - ?.props?.data; + ?.props?.data ?? dataRegistry.get(params.dataKey); return childData ? findNearestStackDatum(params, childData, horizontal) : null; }, [seriesChildren, horizontal], diff --git a/packages/visx-xychart/src/components/series/private/BaseBarGroup.tsx b/packages/visx-xychart/src/components/series/private/BaseBarGroup.tsx index 9280b3891..ab19299e4 100644 --- a/packages/visx-xychart/src/components/series/private/BaseBarGroup.tsx +++ b/packages/visx-xychart/src/components/series/private/BaseBarGroup.tsx @@ -69,12 +69,12 @@ export default function BaseBarGroup< // register all child data useEffect(() => { - const dataToRegister = barSeriesChildren.map((child) => { + barSeriesChildren.forEach((child) => { const { dataKey: key, data, xAccessor, yAccessor } = child.props; - return { key, data, xAccessor, yAccessor }; + const dataToRegister = { key, data, xAccessor, yAccessor }; + if (data && xAccessor && yAccessor) registerData(dataToRegister); }); - registerData(dataToRegister); return () => unregisterData(dataKeys); }, [registerData, unregisterData, barSeriesChildren, dataKeys]); diff --git a/packages/visx-xychart/src/components/series/private/BaseBarSeries.tsx b/packages/visx-xychart/src/components/series/private/BaseBarSeries.tsx index f83b9b6d6..661c52459 100644 --- a/packages/visx-xychart/src/components/series/private/BaseBarSeries.tsx +++ b/packages/visx-xychart/src/components/series/private/BaseBarSeries.tsx @@ -1,5 +1,6 @@ import React, { useContext, useCallback, useMemo } from 'react'; import { AxisScale } from '@visx/axis'; +import { ScaleInput } from '@visx/scale'; import DataContext from '../../../context/DataContext'; import { Bar, BarsProps, SeriesProps } from '../../../types'; import withRegisteredData, { WithRegisteredDataProps } from '../../../enhancers/withRegisteredData'; @@ -46,19 +47,23 @@ function BaseBarSeries & WithRegisteredDataProps) { const { colorScale, + dataRegistry, horizontal, theme, innerWidth = 0, innerHeight = 0, } = useContext(DataContext); + + const xAccessor: (d: Datum) => ScaleInput = _xAccessor ?? dataRegistry.get(dataKey).xAccessor; + const yAccessor: (d: Datum) => ScaleInput = _yAccessor ?? dataRegistry.get(dataKey).yAccessor; const getScaledX = useCallback(getScaledValueFactory(xScale, xAccessor), [xScale, xAccessor]); const getScaledY = useCallback(getScaledValueFactory(yScale, yAccessor), [yScale, yAccessor]); const scaleBandwidth = getScaleBandwidth(horizontal ? yScale : xScale); diff --git a/packages/visx-xychart/src/components/series/private/BaseBarStack.tsx b/packages/visx-xychart/src/components/series/private/BaseBarStack.tsx index b6b433b99..cb21b90a7 100644 --- a/packages/visx-xychart/src/components/series/private/BaseBarStack.tsx +++ b/packages/visx-xychart/src/components/series/private/BaseBarStack.tsx @@ -85,7 +85,7 @@ function BaseBarStack< params: NearestDatumArgs>, ): NearestDatumReturnType => { const childData = seriesChildren.find((child) => child.props.dataKey === params.dataKey) - ?.props?.data; + ?.props?.data ?? dataRegistry.get(params.dataKey); return childData ? findNearestStackDatum(params, childData, horizontal) : null; }, [seriesChildren, horizontal], diff --git a/packages/visx-xychart/src/components/series/private/BaseGlyphSeries.tsx b/packages/visx-xychart/src/components/series/private/BaseGlyphSeries.tsx index 582eab4cc..5778ecf06 100644 --- a/packages/visx-xychart/src/components/series/private/BaseGlyphSeries.tsx +++ b/packages/visx-xychart/src/components/series/private/BaseGlyphSeries.tsx @@ -1,5 +1,6 @@ import React, { useContext, useCallback, useMemo } from 'react'; import { AxisScale } from '@visx/axis'; +import { ScaleInput } from '@visx/scale'; import DataContext from '../../../context/DataContext'; import { GlyphProps, GlyphsProps, SeriesProps } from '../../../types'; import withRegisteredData, { WithRegisteredDataProps } from '../../../enhancers/withRegisteredData'; @@ -37,12 +38,15 @@ export function BaseGlyphSeries< enableEvents = true, renderGlyphs, size = 8, - xAccessor, + xAccessor: _xAccessor, xScale, - yAccessor, + yAccessor: _yAccessor, yScale, }: BaseGlyphSeriesProps & WithRegisteredDataProps) { - const { colorScale, theme, horizontal } = useContext(DataContext); + const { colorScale, dataRegistry, theme, horizontal } = useContext(DataContext); + + const xAccessor: (d: Datum) => ScaleInput = _xAccessor ?? dataRegistry.get(dataKey).xAccessor; + const yAccessor: (d: Datum) => ScaleInput = _yAccessor ?? dataRegistry.get(dataKey).yAccessor; const getScaledX = useCallback(getScaledValueFactory(xScale, xAccessor), [xScale, xAccessor]); const getScaledY = useCallback(getScaledValueFactory(yScale, yAccessor), [yScale, yAccessor]); const color = colorScale?.(dataKey) ?? theme?.colors?.[0] ?? '#222'; diff --git a/packages/visx-xychart/src/components/series/private/BaseLineSeries.tsx b/packages/visx-xychart/src/components/series/private/BaseLineSeries.tsx index 4ab10c141..28d77cc8a 100644 --- a/packages/visx-xychart/src/components/series/private/BaseLineSeries.tsx +++ b/packages/visx-xychart/src/components/series/private/BaseLineSeries.tsx @@ -1,6 +1,7 @@ import React, { useContext, useCallback } from 'react'; import LinePath, { LinePathProps } from '@visx/shape/lib/shapes/LinePath'; import { AxisScale } from '@visx/axis'; +import { ScaleInput } from '@visx/scale'; import DataContext from '../../../context/DataContext'; import { GlyphsProps, SeriesProps } from '../../../types'; import withRegisteredData, { WithRegisteredDataProps } from '../../../enhancers/withRegisteredData'; @@ -32,14 +33,16 @@ function BaseLineSeries & WithRegisteredDataProps) { - const { colorScale, theme } = useContext(DataContext); + const { colorScale, dataRegistry, theme } = useContext(DataContext); + const xAccessor: (d: Datum) => ScaleInput = _xAccessor ?? dataRegistry.get(dataKey).xAccessor; + const yAccessor: (d: Datum) => ScaleInput = _yAccessor ?? dataRegistry.get(dataKey).yAccessor; const getScaledX = useCallback(getScaledValueFactory(xScale, xAccessor), [xScale, xAccessor]); const getScaledY = useCallback(getScaledValueFactory(yScale, yAccessor), [yScale, yAccessor]); const isDefined = useCallback( diff --git a/packages/visx-xychart/src/enhancers/withRegisteredData.tsx b/packages/visx-xychart/src/enhancers/withRegisteredData.tsx index 9ff16971c..0d54eb84c 100644 --- a/packages/visx-xychart/src/enhancers/withRegisteredData.tsx +++ b/packages/visx-xychart/src/enhancers/withRegisteredData.tsx @@ -46,7 +46,9 @@ export default function withRegisteredData< >; useEffect(() => { - if (dataRegistry) dataRegistry.registerData({ key: dataKey, data, xAccessor, yAccessor }); + if (data && xAccessor && yAccessor && dataRegistry) { + dataRegistry.registerData({ key: dataKey, data, xAccessor, yAccessor }); + } return () => dataRegistry?.unregisterData(dataKey); }, [dataRegistry, dataKey, data, xAccessor, yAccessor]); diff --git a/packages/visx-xychart/src/hooks/useStackedData.ts b/packages/visx-xychart/src/hooks/useStackedData.ts index af21d19af..7801f869c 100644 --- a/packages/visx-xychart/src/hooks/useStackedData.ts +++ b/packages/visx-xychart/src/hooks/useStackedData.ts @@ -6,7 +6,12 @@ import { StackPathConfig } from '@visx/shape'; import { extent } from 'd3-array'; import { AxisScale } from '@visx/axis'; import DataContext from '../context/DataContext'; -import { CombinedStackData, DataContextType, SeriesProps } from '../types'; +import { + CombinedStackData, + DataContextType, + DataRegistryEntry, + SeriesProps, +} from '../types'; import getBarStackRegistryData from '../utils/getBarStackRegistryData'; import combineBarStackData from '../utils/combineBarStackData'; import getChildrenAndGrandchildrenWithProps from '../utils/getChildrenAndGrandchildrenWithProps'; @@ -23,7 +28,7 @@ export default function useStackedData< >({ children, order, offset }: UseStackedData) { type StackDatum = SeriesPoint>; - const { horizontal, registerData, unregisterData } = useContext( + const { horizontal, dataRegistry, registerData, unregisterData } = useContext( DataContext, ) as unknown as DataContextType; @@ -34,17 +39,28 @@ export default function useStackedData< [children], ); - // extract data keys from child series - const dataKeys: string[] = useMemo( - () => seriesChildren.filter((child) => child.props.dataKey).map((child) => child.props.dataKey), + const stackedSeries: DataRegistryEntry[] = useMemo( + () => seriesChildren + .filter((child) => child.props.dataKey) + .map((child) => ( + 'data' in child.props + ? {key: child.props.dataKey, ...child.props} + : dataRegistry.get(child.props.dataKey) + )), [seriesChildren], ); + // extract data keys from child series + const dataKeys = useMemo( + () => stackedSeries.map((series) => series.key), + [stackedSeries], + ); + // group all child data by stack value { [x | y]: { [dataKey]: value } } // this format is needed by d3Stack const combinedData = useMemo( - () => combineBarStackData(seriesChildren, horizontal), - [horizontal, seriesChildren], + () => combineBarStackData(stackedSeries, horizontal), + [stackedSeries, horizontal], ); // stack data diff --git a/packages/visx-xychart/src/providers/DataProvider.tsx b/packages/visx-xychart/src/providers/DataProvider.tsx index 86b072721..be6ed9e2e 100644 --- a/packages/visx-xychart/src/providers/DataProvider.tsx +++ b/packages/visx-xychart/src/providers/DataProvider.tsx @@ -2,8 +2,9 @@ import { ScaleConfig, ScaleConfigToD3Scale } from '@visx/scale'; import React, { useContext, useMemo } from 'react'; import createOrdinalScale from '@visx/scale/lib/scales/ordinal'; -import { AxisScaleOutput } from '@visx/axis'; +import { AxisScale, AxisScaleOutput } from '@visx/axis'; import { XYChartTheme } from '../types'; +import { DataRegistryEntry } from '../types/data'; import ThemeContext from '../context/ThemeContext'; import DataContext from '../context/DataContext'; import useDataRegistry from '../hooks/useDataRegistry'; @@ -15,6 +16,7 @@ import isDiscreteScale from '../utils/isDiscreteScale'; export type DataProviderProps< XScaleConfig extends ScaleConfig, YScaleConfig extends ScaleConfig, + Datum extends object, > = { /* Optionally define the initial dimensions. */ initialDimensions?: Partial; @@ -24,6 +26,10 @@ export type DataProviderProps< xScale: XScaleConfig; /* y-scale configuration whose shape depends on scale type. */ yScale: YScaleConfig; + /** XYChart data to be rendered in Series. */ + data?: + | DataRegistryEntry + | DataRegistryEntry[]; /* Any React children. */ children: React.ReactNode; /* Determines whether Series will be plotted horizontally (e.g., horizontal bars). By default this will try to be inferred based on scale types. */ @@ -39,9 +45,10 @@ export default function DataProvider< theme: propsTheme, xScale: xScaleConfig, yScale: yScaleConfig, + data, children, horizontal: initialHorizontal = 'auto', -}: DataProviderProps) { +}: DataProviderProps) { // `DataProvider` provides a theme so that `ThemeProvider` is not strictly needed. // `props.theme` takes precedent over `context.theme`, which has a default even if // a ThemeProvider is not present. @@ -56,6 +63,10 @@ export default function DataProvider< const dataRegistry = useDataRegistry(); + useMemo(() => { + if (data) dataRegistry.registerData(data) + }, [data]); + const { xScale, yScale }: { xScale?: XScale; yScale?: YScale } = useScales({ dataRegistry, xScaleConfig, diff --git a/packages/visx-xychart/src/types/series.ts b/packages/visx-xychart/src/types/series.ts index 8d7c9c007..18fee60ff 100644 --- a/packages/visx-xychart/src/types/series.ts +++ b/packages/visx-xychart/src/types/series.ts @@ -29,11 +29,11 @@ export type SeriesProps< /** Required data key for the Series, should be unique across all series. */ dataKey: string; /** Data for the Series. */ - data: Datum[]; + data?: Datum[]; /** Given a Datum, returns the x-scale value. */ - xAccessor: (d: Datum) => ScaleInput; + xAccessor?: (d: Datum) => ScaleInput; /** Given a Datum, returns the y-scale value. */ - yAccessor: (d: Datum) => ScaleInput; + yAccessor?: (d: Datum) => ScaleInput; /** * Callback invoked for onPointerMove events for the nearest Datum to the PointerEvent. * By default XYChart will capture and emit PointerEvents, invoking this function for diff --git a/packages/visx-xychart/src/utils/combineBarStackData.ts b/packages/visx-xychart/src/utils/combineBarStackData.ts index dc52ae392..d057d2198 100644 --- a/packages/visx-xychart/src/utils/combineBarStackData.ts +++ b/packages/visx-xychart/src/utils/combineBarStackData.ts @@ -1,6 +1,6 @@ import React from 'react'; import { AxisScale } from '@visx/axis'; -import { CombinedStackData, SeriesProps } from '../types'; +import { CombinedStackData, DataRegistryEntry, SeriesProps } from '../types'; /** Returns the value which forms a stack group. */ export const getStackValue = ( @@ -17,15 +17,15 @@ export default function combineBarStackData< YScale extends AxisScale, Datum extends object, >( - seriesChildren: React.ReactElement>[], + stackedSeries: DataRegistryEntry[], horizontal?: boolean, ): CombinedStackData[] { const dataByStackValue: { [stackValue: string]: CombinedStackData; } = {}; - seriesChildren.forEach((child) => { - const { dataKey, data, xAccessor, yAccessor } = child.props; + stackedSeries.forEach((series) => { + const { key, data, xAccessor, yAccessor } = series; // this should exist but double check if (!xAccessor || !yAccessor) return; @@ -39,7 +39,7 @@ export default function combineBarStackData< if (!dataByStackValue[stackKey]) { dataByStackValue[stackKey] = { stack, positiveSum: 0, negativeSum: 0 }; } - dataByStackValue[stackKey][dataKey] = numericValue; + dataByStackValue[stackKey][key] = numericValue; dataByStackValue[stackKey][numericValue >= 0 ? 'positiveSum' : 'negativeSum'] += numericValue; }); });