From e3ce7036c374fc73619533f75dbacf0bcec5764d Mon Sep 17 00:00:00 2001 From: plouc Date: Sat, 24 May 2025 07:40:06 +0900 Subject: [PATCH 1/4] feat(bar): always use a default for the datum type and add introduce generic type for the index value --- packages/bar/src/Bar.tsx | 27 +-- packages/bar/src/BarAnnotations.tsx | 6 +- packages/bar/src/BarCanvas.tsx | 37 ++-- packages/bar/src/BarItem.tsx | 6 +- packages/bar/src/BarTooltip.tsx | 8 +- packages/bar/src/BarTotals.tsx | 10 +- packages/bar/src/ResponsiveBar.tsx | 14 +- packages/bar/src/ResponsiveBarCanvas.tsx | 12 +- packages/bar/src/defaults.ts | 46 +++-- packages/bar/src/hooks.ts | 5 +- packages/bar/src/renderBar.ts | 6 +- packages/bar/src/types.ts | 233 +++++++++++++---------- 12 files changed, 231 insertions(+), 179 deletions(-) diff --git a/packages/bar/src/Bar.tsx b/packages/bar/src/Bar.tsx index f7bec8c42..111ef2bf5 100644 --- a/packages/bar/src/Bar.tsx +++ b/packages/bar/src/Bar.tsx @@ -20,6 +20,7 @@ import { BarComponent, BarCustomLayerProps, BarDatum, + BarIndex, BarItemProps, BarLayerId, BarSvgProps, @@ -29,12 +30,12 @@ import { import { BarTotals } from './BarTotals' import { useComputeLabelLayout } from './compute/common' -type InnerBarProps = Omit< - BarSvgProps, +type InnerBarProps = Omit< + BarSvgProps, 'animate' | 'motionConfig' | 'renderWrapper' | 'theme' > -const InnerBar = ({ +const InnerBar = ({ data, indexBy, keys, @@ -56,7 +57,7 @@ const InnerBar = ({ gridXValues, gridYValues, layers = svgDefaultProps.layers as BarLayerId[], - barComponent = svgDefaultProps.barComponent as unknown as BarComponent, + barComponent = svgDefaultProps.barComponent as unknown as BarComponent, enableLabel = svgDefaultProps.enableLabel, label, labelSkipWidth = svgDefaultProps.labelSkipWidth, @@ -77,7 +78,7 @@ const InnerBar = ({ tooltipLabel, valueFormat, isInteractive = svgDefaultProps.isInteractive, - tooltip = svgDefaultProps.tooltip as BarTooltipComponent, + tooltip = svgDefaultProps.tooltip as BarTooltipComponent, onClick, onMouseEnter, onMouseLeave, @@ -96,7 +97,7 @@ const InnerBar = ({ enableTotals = svgDefaultProps.enableTotals, totalsOffset = svgDefaultProps.totalsOffset, forwardedRef, -}: InnerBarProps & { +}: InnerBarProps & { forwardedRef: Ref }) => { const { animate, config: springConfig } = useMotionConfig() @@ -120,7 +121,7 @@ const InnerBar = ({ legendsWithData, barTotals, getColor, - } = useBar({ + } = useBar({ indexBy, label, tooltipLabel, @@ -380,7 +381,7 @@ const InnerBar = ({ ) } - const layerContext: BarCustomLayerProps = { + const layerContext: BarCustomLayerProps = { ...commonProps, margin, width, @@ -425,7 +426,7 @@ const InnerBar = ({ } export const Bar = forwardRef( - ( + ( { isInteractive = svgDefaultProps.isInteractive, animate = svgDefaultProps.animate, @@ -433,7 +434,7 @@ export const Bar = forwardRef( theme, renderWrapper, ...props - }: BarSvgProps, + }: BarSvgProps, ref: Ref ) => ( - {...props} isInteractive={isInteractive} forwardedRef={ref} /> + {...props} isInteractive={isInteractive} forwardedRef={ref} /> ) -) as (props: WithChartRef, SVGSVGElement>) => ReactElement +) as ( + props: WithChartRef, SVGSVGElement> +) => ReactElement diff --git a/packages/bar/src/BarAnnotations.tsx b/packages/bar/src/BarAnnotations.tsx index 49f9882bf..350ad4ed6 100644 --- a/packages/bar/src/BarAnnotations.tsx +++ b/packages/bar/src/BarAnnotations.tsx @@ -1,10 +1,10 @@ import { Annotation, useAnnotations } from '@nivo/annotations' -import { BarAnnotationsProps, BarDatum } from './types' +import { BarAnnotationsProps, BarDatum, BarIndex } from './types' -export const BarAnnotations = ({ +export const BarAnnotations = ({ bars, annotations, -}: BarAnnotationsProps) => { +}: BarAnnotationsProps) => { const boundAnnotations = useAnnotations({ data: bars, annotations, diff --git a/packages/bar/src/BarCanvas.tsx b/packages/bar/src/BarCanvas.tsx index 85309a357..0033eb65e 100644 --- a/packages/bar/src/BarCanvas.tsx +++ b/packages/bar/src/BarCanvas.tsx @@ -36,6 +36,7 @@ import { BarCanvasRenderer, BarCommonProps, BarDatum, + BarIndex, BarTooltipComponent, ComputedBarDatum, } from './types' @@ -43,8 +44,8 @@ import { useBar } from './hooks' import { BarTotalsData } from './compute/totals' import { useComputeLabelLayout } from './compute/common' -const findBarUnderCursor = ( - nodes: ComputedBarDatum[], +const findBarUnderCursor = ( + nodes: readonly ComputedBarDatum[], margin: Margin, x: number, y: number @@ -55,11 +56,11 @@ const findBarUnderCursor = ( const isNumber = (value: unknown): value is number => typeof value === 'number' -function renderTotalsToCanvas( +function renderTotalsToCanvas( ctx: CanvasRenderingContext2D, - barTotals: BarTotalsData[], + barTotals: readonly BarTotalsData[], theme: Theme, - layout: BarCommonProps['layout'] = canvasDefaultProps.layout + layout: BarCommonProps['layout'] = canvasDefaultProps.layout ) { setCanvasFont(ctx, theme.labels.text) ctx.textBaseline = layout === 'vertical' ? 'alphabetic' : 'middle' @@ -70,14 +71,14 @@ function renderTotalsToCanvas( }) } -type InnerBarCanvasProps = Omit< - BarCanvasProps, +type InnerBarCanvasProps = Omit< + BarCanvasProps, 'renderWrapper' | 'theme' > & { forwardedRef: Ref } -const InnerBarCanvas = ({ +const InnerBarCanvas = ({ data, indexBy, keys, @@ -100,8 +101,8 @@ const InnerBarCanvas = ({ gridYValues, labelPosition = canvasDefaultProps.labelPosition, labelOffset = canvasDefaultProps.labelOffset, - layers = canvasDefaultProps.layers as BarCanvasLayer[], - renderBar = canvasDefaultProps.renderBar as unknown as BarCanvasRenderer, + layers = canvasDefaultProps.layers as BarCanvasLayer[], + renderBar = canvasDefaultProps.renderBar as unknown as BarCanvasRenderer, enableLabel = canvasDefaultProps.enableLabel, label, labelSkipWidth = canvasDefaultProps.labelSkipWidth, @@ -117,7 +118,7 @@ const InnerBarCanvas = ({ tooltipLabel, valueFormat, isInteractive = canvasDefaultProps.isInteractive, - tooltip = canvasDefaultProps.tooltip as BarTooltipComponent, + tooltip = canvasDefaultProps.tooltip as BarTooltipComponent, onClick, onMouseEnter, onMouseLeave, @@ -127,7 +128,7 @@ const InnerBarCanvas = ({ forwardedRef, enableTotals = canvasDefaultProps.enableTotals, totalsOffset = canvasDefaultProps.totalsOffset, -}: InnerBarCanvasProps) => { +}: InnerBarCanvasProps) => { const canvasEl = useRef(null) const theme = useTheme() @@ -150,7 +151,7 @@ const InnerBarCanvas = ({ legendsWithData, barTotals, getColor, - } = useBar({ + } = useBar({ indexBy, label, tooltipLabel, @@ -468,8 +469,8 @@ const InnerBarCanvas = ({ } export const BarCanvas = forwardRef( - ( - { isInteractive, renderWrapper, theme, ...props }: BarCanvasProps, + ( + { isInteractive, renderWrapper, theme, ...props }: BarCanvasProps, ref: Ref ) => ( - {...props} isInteractive={isInteractive} forwardedRef={ref} /> + {...props} isInteractive={isInteractive} forwardedRef={ref} /> ) -) as ( - props: WithChartRef, HTMLCanvasElement> +) as ( + props: WithChartRef, HTMLCanvasElement> ) => ReactElement diff --git a/packages/bar/src/BarItem.tsx b/packages/bar/src/BarItem.tsx index 02d88f1e3..fe4e8619b 100644 --- a/packages/bar/src/BarItem.tsx +++ b/packages/bar/src/BarItem.tsx @@ -3,9 +3,9 @@ import { animated, to } from '@react-spring/web' import { useTheme } from '@nivo/theming' import { useTooltip } from '@nivo/tooltip' import { Text } from '@nivo/text' -import { BarDatum, BarItemProps } from './types' +import { BarDatum, BarIndex, BarItemProps } from './types' -export const BarItem = ({ +export const BarItem = ({ bar: { data, ...bar }, style: { borderColor, @@ -34,7 +34,7 @@ export const BarItem = ({ ariaDescribedBy, ariaDisabled, ariaHidden, -}: BarItemProps) => { +}: BarItemProps) => { const theme = useTheme() const { showTooltipFromEvent, showTooltipAt, hideTooltip } = useTooltip() diff --git a/packages/bar/src/BarTooltip.tsx b/packages/bar/src/BarTooltip.tsx index a983ce42c..aa45143ff 100644 --- a/packages/bar/src/BarTooltip.tsx +++ b/packages/bar/src/BarTooltip.tsx @@ -1,6 +1,10 @@ import { BasicTooltip } from '@nivo/tooltip' -import { BarDatum, BarTooltipProps } from './types' +import { BarDatum, BarIndex, BarTooltipProps } from './types' -export const BarTooltip = ({ color, label, ...data }: BarTooltipProps) => { +export const BarTooltip = ({ + color, + label, + ...data +}: BarTooltipProps) => { return } diff --git a/packages/bar/src/BarTotals.tsx b/packages/bar/src/BarTotals.tsx index 5efa9d610..c1738c5ef 100644 --- a/packages/bar/src/BarTotals.tsx +++ b/packages/bar/src/BarTotals.tsx @@ -1,22 +1,22 @@ import { useTheme } from '@nivo/theming' import { AnimationConfig, animated, useTransition } from '@react-spring/web' -import { BarCommonProps, BarDatum } from './types' +import { BarCommonProps, BarDatum, BarIndex } from './types' import { svgDefaultProps } from './defaults' import { BarTotalsData } from './compute/totals' -interface Props { +interface Props { data: BarTotalsData[] springConfig: Partial animate: boolean - layout?: BarCommonProps['layout'] + layout?: BarCommonProps['layout'] } -export const BarTotals = ({ +export const BarTotals = ({ data, springConfig, animate, layout = svgDefaultProps.layout, -}: Props) => { +}: Props) => { const theme = useTheme() const totalsTransition = useTransition< BarTotalsData, diff --git a/packages/bar/src/ResponsiveBar.tsx b/packages/bar/src/ResponsiveBar.tsx index 92dbd795d..3584245df 100644 --- a/packages/bar/src/ResponsiveBar.tsx +++ b/packages/bar/src/ResponsiveBar.tsx @@ -1,17 +1,17 @@ import { forwardRef, Ref, ReactElement } from 'react' import { ResponsiveWrapper } from '@nivo/core' import { Bar } from './Bar' -import { BarDatum, ResponsiveBarSvgProps } from './types' +import { BarDatum, BarIndex, ResponsiveBarSvgProps } from './types' export const ResponsiveBar = forwardRef( - ( + ( { defaultWidth, defaultHeight, onResize, debounceResize, ...props - }: Omit, 'ref'>, + }: Omit, 'ref'>, ref: Ref ) => ( - {({ width, height }) => {...props} width={width} height={height} ref={ref} />} + {({ width, height }) => ( + {...props} width={width} height={height} ref={ref} /> + )} ) -) as (props: ResponsiveBarSvgProps) => ReactElement +) as ( + props: ResponsiveBarSvgProps +) => ReactElement diff --git a/packages/bar/src/ResponsiveBarCanvas.tsx b/packages/bar/src/ResponsiveBarCanvas.tsx index 1e7b92400..b26a7a7c2 100644 --- a/packages/bar/src/ResponsiveBarCanvas.tsx +++ b/packages/bar/src/ResponsiveBarCanvas.tsx @@ -1,17 +1,17 @@ import { ForwardedRef, forwardRef, ReactElement } from 'react' import { ResponsiveWrapper } from '@nivo/core' -import { BarDatum, ResponsiveBarCanvasProps } from './types' +import { BarDatum, BarIndex, ResponsiveBarCanvasProps } from './types' import { BarCanvas } from './BarCanvas' export const ResponsiveBarCanvas = forwardRef( - ( + ( { defaultWidth, defaultHeight, onResize, debounceResize, ...props - }: Omit, 'ref'>, + }: Omit, 'ref'>, ref: ForwardedRef ) => ( {({ width, height }) => ( - {...props} width={width} height={height} ref={ref} /> + {...props} width={width} height={height} ref={ref} /> )} ) -) as (props: ResponsiveBarCanvasProps) => ReactElement +) as ( + props: ResponsiveBarCanvasProps +) => ReactElement diff --git a/packages/bar/src/defaults.ts b/packages/bar/src/defaults.ts index 41434ccc9..40c9ad758 100644 --- a/packages/bar/src/defaults.ts +++ b/packages/bar/src/defaults.ts @@ -10,7 +10,7 @@ import { BarItem } from './BarItem' import { BarTooltip } from './BarTooltip' import { renderBar } from './renderBar' -export const commonDefaultProps: Omit, 'data' | 'theme'> = { +export const commonDefaultProps: Omit = { indexBy: 'id', keys: ['value'], groupMode: 'stacked' as const, @@ -35,37 +35,35 @@ export const commonDefaultProps: Omit, 'data' | 'theme' borderColor: { from: 'color' } as InheritedColorConfig, isInteractive: true, tooltip: BarTooltip, - tooltipLabel: (datum: ComputedDatum) => `${datum.id} - ${datum.indexValue}`, + tooltipLabel: (datum: ComputedDatum) => `${datum.id} - ${datum.indexValue}`, legends: [], initialHiddenIds: [], annotations: [], - enableTotals: false, + enableTotals: true, totalsOffset: 10, } -export const svgDefaultProps: Omit< - BarSvgPropsWithDefaults, - 'data' | 'width' | 'height' | 'theme' -> = { - ...commonDefaultProps, - layers: ['grid', 'axes', 'bars', 'totals', 'markers', 'legends', 'annotations'], - axisTop: null, - axisRight: null, - axisBottom: {}, - axisLeft: {}, - barComponent: BarItem, - defs: [], - fill: [], - markers: [], - animate: true, - animateOnMount: false, - motionConfig: 'default', - role: 'img', - isFocusable: false, -} +export const svgDefaultProps: Omit = + { + ...commonDefaultProps, + layers: ['grid', 'axes', 'bars', 'totals', 'markers', 'legends', 'annotations'], + axisTop: null, + axisRight: null, + axisBottom: {}, + axisLeft: {}, + barComponent: BarItem, + defs: [], + fill: [], + markers: [], + animate: true, + animateOnMount: false, + motionConfig: 'default', + role: 'img', + isFocusable: false, + } export const canvasDefaultProps: Omit< - BarCanvasPropsWithDefaults, + BarCanvasPropsWithDefaults, 'data' | 'width' | 'height' | 'theme' > = { ...commonDefaultProps, diff --git a/packages/bar/src/hooks.ts b/packages/bar/src/hooks.ts index b4085cad6..dd7cd3db3 100644 --- a/packages/bar/src/hooks.ts +++ b/packages/bar/src/hooks.ts @@ -9,12 +9,13 @@ import { ComputedBarDatumWithValue, LegendData, BarLegendProps, + BarIndex, } from './types' import { commonDefaultProps } from './defaults' import { generateGroupedBars, generateStackedBars, getLegendData } from './compute' import { computeBarTotals } from './compute/totals' -export const useBar = ({ +export const useBar = ({ indexBy = commonDefaultProps.indexBy, keys = commonDefaultProps.keys, label = commonDefaultProps.label, @@ -43,7 +44,7 @@ export const useBar = ({ totalsOffset = commonDefaultProps.totalsOffset, }: Partial< Pick< - BarCommonProps, + BarCommonProps, | 'indexBy' | 'keys' | 'label' diff --git a/packages/bar/src/renderBar.ts b/packages/bar/src/renderBar.ts index c4bd95179..b61b3492c 100644 --- a/packages/bar/src/renderBar.ts +++ b/packages/bar/src/renderBar.ts @@ -1,8 +1,8 @@ import { roundedRect } from '@nivo/canvas' import { drawCanvasText } from '@nivo/text' -import { BarDatum, RenderBarProps } from './types' +import { BarDatum, BarIndex, RenderBarProps } from './types' -export const renderBar = ( +export const renderBar = ( ctx: CanvasRenderingContext2D, { bar: { color, height, width, x, y }, @@ -15,7 +15,7 @@ export const renderBar = ( labelX, labelY, textAnchor, - }: RenderBarProps + }: RenderBarProps ) => { ctx.fillStyle = color if (borderWidth > 0) { diff --git a/packages/bar/src/types.ts b/packages/bar/src/types.ts index 0f40befbc..2420449c6 100644 --- a/packages/bar/src/types.ts +++ b/packages/bar/src/types.ts @@ -20,33 +20,46 @@ import { AnyScale, ScaleLinearSpec, ScaleSymlogSpec, ScaleBandSpec } from '@nivo import { SpringValues } from '@react-spring/web' import { BarLabelLayout } from './compute/common' -export type BarDatum = Record +// We can use any type for the index, but we need to pick +// an appropriate scale for the type we choose. +// The default is a string. +// The index scale is converted to a band scale eventually, +// because bars need to be spaced evenly. +export type BarIndex = string | number | Date +// We only support numbers for the value, but we can +// also use null to indicate that the value is missing. +export type BarValue = number | null +// Default datum type for the bar chart. +export type BarDatum = Record export interface DataProps { data: readonly D[] } -export type ComputedDatum = { +export type ComputedDatum = { id: string | number - value: number | null + value: BarValue formattedValue: string hidden: boolean index: number - indexValue: string | number + indexValue: I data: Exclude fill?: string } -export type ComputedBarDatumWithValue = ComputedBarDatum & { - data: ComputedDatum & { +export type ComputedBarDatumWithValue< + D extends BarDatum = BarDatum, + I extends BarIndex = string, +> = ComputedBarDatum & { + data: ComputedDatum & { value: number } } -export type ComputedBarDatum = { +export type ComputedBarDatum = { key: string index: number - data: ComputedDatum + data: ComputedDatum x: number y: number absX: number @@ -57,7 +70,7 @@ export type ComputedBarDatum = { label: string } -export type BarsWithHidden = Array< +export type BarsWithHidden = Array< Partial<{ key: string x: number @@ -66,14 +79,16 @@ export type BarsWithHidden = Array< height: number color: string }> & { - data: Partial> & { + data: Partial> & { id: string | number hidden: boolean } } > -export type LegendLabelDatum = Partial> & { +export type LegendLabelDatum = Partial< + ComputedDatum +> & { id: string | number hidden: boolean } @@ -94,9 +109,9 @@ export type LabelFormatter = (label: string | number) => string | number export type BarLayerId = 'grid' | 'axes' | 'bars' | 'markers' | 'legends' | 'annotations' | 'totals' export type BarCanvasLayerId = Exclude -interface BarCustomLayerBaseProps +interface BarCustomLayerBaseProps extends Pick< - BarCommonProps, + BarCommonProps, | 'borderRadius' | 'borderWidth' | 'enableLabel' @@ -106,39 +121,51 @@ interface BarCustomLayerBaseProps | 'tooltip' >, Dimensions { - bars: readonly ComputedBarDatum[] + bars: readonly ComputedBarDatum[] legendData: [BarLegendProps, readonly LegendData[]][] margin: Margin innerWidth: number innerHeight: number isFocusable: boolean - getTooltipLabel: (datum: ComputedDatum) => string | number + getTooltipLabel: (datum: ComputedDatum) => string | number xScale: AnyScale yScale: AnyScale - getColor: OrdinalColorScale> + getColor: OrdinalColorScale> } -export interface BarCustomLayerProps - extends BarCustomLayerBaseProps, - BarHandlers {} +export interface BarCustomLayerProps + extends BarCustomLayerBaseProps, + BarHandlers {} -export interface BarCanvasCustomLayerProps - extends BarCustomLayerBaseProps, - BarHandlers {} +export interface BarCanvasCustomLayerProps< + D extends BarDatum = BarDatum, + I extends BarIndex = string, +> extends BarCustomLayerBaseProps, + BarHandlers {} -export type BarCanvasCustomLayer = ( +export type BarCanvasCustomLayer = ( context: CanvasRenderingContext2D, - props: BarCanvasCustomLayerProps + props: BarCanvasCustomLayerProps ) => void -export type BarCustomLayer = FunctionComponent> +export type BarCustomLayer< + D extends BarDatum = BarDatum, + I extends BarIndex = string, +> = FunctionComponent> -export type BarCanvasLayer = BarCanvasLayerId | BarCanvasCustomLayer -export type BarLayer = BarLayerId | BarCustomLayer +export type BarCanvasLayer = + | BarCanvasLayerId + | BarCanvasCustomLayer +export type BarLayer = + | BarLayerId + | BarCustomLayer -export interface BarItemProps - extends Pick, 'borderRadius' | 'borderWidth' | 'isInteractive' | 'tooltip'>, - BarHandlers { - bar: ComputedBarDatum & { +export interface BarItemProps + extends Pick< + BarCommonProps, + 'borderRadius' | 'borderWidth' | 'isInteractive' | 'tooltip' + >, + BarHandlers { + bar: ComputedBarDatum & { data: { value: number } @@ -159,16 +186,19 @@ export interface BarItemProps label: string shouldRenderLabel: boolean isFocusable: boolean - ariaLabel?: BarSvgProps['barAriaLabel'] - ariaLabelledBy?: BarSvgProps['barAriaLabelledBy'] - ariaDescribedBy?: BarSvgProps['barAriaDescribedBy'] - ariaHidden?: BarSvgProps['barAriaHidden'] - ariaDisabled?: BarSvgProps['barAriaDisabled'] + ariaLabel?: BarSvgProps['barAriaLabel'] + ariaLabelledBy?: BarSvgProps['barAriaLabelledBy'] + ariaDescribedBy?: BarSvgProps['barAriaDescribedBy'] + ariaHidden?: BarSvgProps['barAriaHidden'] + ariaDisabled?: BarSvgProps['barAriaDisabled'] } -export type BarComponent = FunctionComponent> +export type BarComponent< + D extends BarDatum = BarDatum, + I extends BarIndex = string, +> = FunctionComponent> -export type RenderBarProps = Omit< - BarItemProps, +export type RenderBarProps = Omit< + BarItemProps, | 'isInteractive' | 'style' | 'tooltip' @@ -183,26 +213,30 @@ export type RenderBarProps = Omit< borderColor: string labelStyle: TextStyle } -export type BarCanvasRenderer = ( +export type BarCanvasRenderer = ( context: CanvasRenderingContext2D, - props: RenderBarProps + props: RenderBarProps ) => void -export interface BarTooltipProps extends ComputedDatum { +export interface BarTooltipProps + extends ComputedDatum { color: string label: string value: number } -export type BarTooltipComponent = FunctionComponent> +export type BarTooltipComponent< + D extends BarDatum = BarDatum, + I extends BarIndex = string, +> = FunctionComponent> -export type BarHandlers = { - onClick?: (datum: ComputedDatum & { color: string }, event: MouseEvent) => void - onMouseEnter?: (datum: ComputedDatum, event: MouseEvent) => void - onMouseLeave?: (datum: ComputedDatum, event: MouseEvent) => void +export type BarHandlers = { + onClick?: (datum: ComputedDatum & { color: string }, event: MouseEvent) => void + onMouseEnter?: (datum: ComputedDatum, event: MouseEvent) => void + onMouseLeave?: (datum: ComputedDatum, event: MouseEvent) => void } -export type BarCommonProps = { - indexBy: PropertyAccessor +export type BarCommonProps = { + indexBy: PropertyAccessor keys: readonly string[] margin?: Box innerPadding: number @@ -217,24 +251,24 @@ export type BarCommonProps = { borderRadius: number borderWidth: number enableLabel: boolean - label: PropertyAccessor, string> + label: PropertyAccessor, string> labelPosition: 'start' | 'middle' | 'end' labelOffset: number labelFormat?: string | LabelFormatter labelSkipWidth: number labelSkipHeight: number - labelTextColor: InheritedColorConfig> + labelTextColor: InheritedColorConfig> isInteractive: boolean - tooltip: BarTooltipComponent + tooltip: BarTooltipComponent valueFormat?: ValueFormat - legendLabel?: PropertyAccessor, string> - tooltipLabel: PropertyAccessor, string> + legendLabel?: PropertyAccessor, string> + tooltipLabel: PropertyAccessor, string> groupMode: 'grouped' | 'stacked' layout: 'horizontal' | 'vertical' colorBy: 'id' | 'indexValue' - colors: OrdinalColorScaleConfig> + colors: OrdinalColorScaleConfig> theme: PartialTheme - annotations: readonly AnnotationMatcher>[] + annotations: readonly AnnotationMatcher>[] legends: readonly BarLegendProps[] renderWrapper?: boolean initialHiddenIds: readonly (string | number)[] @@ -243,71 +277,76 @@ export type BarCommonProps = { role?: string } -interface BarSvgExtraProps { +interface BarSvgExtraProps { axisBottom: AxisProps | null axisLeft: AxisProps | null axisRight: AxisProps | null axisTop: AxisProps | null - barComponent: BarComponent + barComponent: BarComponent markers: readonly CartesianMarkerProps[] - layers: readonly BarLayer[] + layers: readonly BarLayer[] animateOnMount: boolean ariaLabel?: AriaAttributes['aria-label'] ariaLabelledBy?: AriaAttributes['aria-labelledby'] ariaDescribedBy?: AriaAttributes['aria-describedby'] isFocusable: boolean - barRole?: string | ((data: ComputedDatum) => string) - barAriaLabel?: (data: ComputedDatum) => AriaAttributes['aria-label'] - barAriaLabelledBy?: (data: ComputedDatum) => AriaAttributes['aria-labelledby'] - barAriaDescribedBy?: (data: ComputedDatum) => AriaAttributes['aria-describedby'] - barAriaHidden?: (data: ComputedDatum) => AriaAttributes['aria-hidden'] - barAriaDisabled?: (data: ComputedDatum) => AriaAttributes['aria-disabled'] + barRole?: string | ((data: ComputedDatum) => string) + barAriaLabel?: (data: ComputedDatum) => AriaAttributes['aria-label'] + barAriaLabelledBy?: (data: ComputedDatum) => AriaAttributes['aria-labelledby'] + barAriaDescribedBy?: (data: ComputedDatum) => AriaAttributes['aria-describedby'] + barAriaHidden?: (data: ComputedDatum) => AriaAttributes['aria-hidden'] + barAriaDisabled?: (data: ComputedDatum) => AriaAttributes['aria-disabled'] } -export type BarSvgProps = DataProps & - Partial> & - Partial> & - BarHandlers & - SvgDefsAndFill> & +export type BarSvgProps = DataProps & + Partial> & + Partial> & + BarHandlers & + SvgDefsAndFill> & Dimensions & MotionProps -export type ResponsiveBarSvgProps = WithChartRef< - ResponsiveProps>, - SVGSVGElement -> -export type BarSvgPropsWithDefaults = DataProps & - BarCommonProps & - BarSvgExtraProps & - SvgDefsAndFill> & +export type ResponsiveBarSvgProps< + D extends BarDatum = BarDatum, + I extends BarIndex = string, +> = WithChartRef>, SVGSVGElement> +export type BarSvgPropsWithDefaults< + D extends BarDatum = BarDatum, + I extends BarIndex = string, +> = DataProps & + BarCommonProps & + BarSvgExtraProps & + SvgDefsAndFill> & Dimensions & MotionProps -interface BarCanvasExtraProps { +interface BarCanvasExtraProps { axisBottom: CanvasAxisProps | null axisLeft: CanvasAxisProps | null axisRight: CanvasAxisProps | null axisTop: CanvasAxisProps | null - renderBar: BarCanvasRenderer - layers: BarCanvasLayer[] + renderBar: BarCanvasRenderer + layers: BarCanvasLayer[] pixelRatio: number } -export type BarCanvasProps = DataProps & - Partial> & - Partial> & - BarHandlers & - Dimensions -export type ResponsiveBarCanvasProps = WithChartRef< - ResponsiveProps>, - HTMLCanvasElement -> -export type BarCanvasPropsWithDefaults = DataProps & - BarCommonProps & - BarCanvasExtraProps & - BarHandlers & +export type BarCanvasProps< + D extends BarDatum = BarDatum, + I extends BarIndex = string, +> = DataProps & + Partial> & + Partial> & + BarHandlers & Dimensions +export type ResponsiveBarCanvasProps< + D extends BarDatum = BarDatum, + I extends BarIndex = string, +> = WithChartRef>, HTMLCanvasElement> +export type BarCanvasPropsWithDefaults< + D extends BarDatum = BarDatum, + I extends BarIndex = string, +> = DataProps & BarCommonProps & BarCanvasExtraProps & BarHandlers & Dimensions -export type BarAnnotationsProps = { - annotations: readonly AnnotationMatcher>[] - bars: readonly ComputedBarDatum[] +export type BarAnnotationsProps = { + annotations: readonly AnnotationMatcher>[] + bars: readonly ComputedBarDatum[] } From 5a213456e9f8b59cdb15321bbaffd58684f0c78c Mon Sep 17 00:00:00 2001 From: plouc Date: Sat, 24 May 2025 07:57:45 +0900 Subject: [PATCH 2/4] feat(bar): add typings for more interaction handlers --- packages/bar/src/types.ts | 48 ++++++++++++++++++++++++++++----------- 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/packages/bar/src/types.ts b/packages/bar/src/types.ts index 2420449c6..372a51a40 100644 --- a/packages/bar/src/types.ts +++ b/packages/bar/src/types.ts @@ -1,4 +1,5 @@ -import { MouseEvent, AriaAttributes, FunctionComponent } from 'react' +import { AriaAttributes, FunctionComponent } from 'react' +import { SpringValues } from '@react-spring/web' import { AnnotationMatcher } from '@nivo/annotations' import { AxisProps, CanvasAxisProps, GridValues } from '@nivo/axes' import { @@ -12,12 +13,13 @@ import { ValueFormat, WithChartRef, ResponsiveProps, + EventMap, + InteractionHandlers, } from '@nivo/core' import { PartialTheme, TextStyle } from '@nivo/theming' import { InheritedColorConfig, OrdinalColorScale, OrdinalColorScaleConfig } from '@nivo/colors' import { LegendProps } from '@nivo/legends' import { AnyScale, ScaleLinearSpec, ScaleSymlogSpec, ScaleBandSpec } from '@nivo/scales' -import { SpringValues } from '@react-spring/web' import { BarLabelLayout } from './compute/common' // We can use any type for the index, but we need to pick @@ -135,13 +137,13 @@ interface BarCustomLayerBaseProps extends BarCustomLayerBaseProps, - BarHandlers {} + BarInteractionHandlers {} export interface BarCanvasCustomLayerProps< D extends BarDatum = BarDatum, I extends BarIndex = string, > extends BarCustomLayerBaseProps, - BarHandlers {} + BarInteractionHandlers {} export type BarCanvasCustomLayer = ( context: CanvasRenderingContext2D, @@ -164,7 +166,7 @@ export interface BarItemProps, 'borderRadius' | 'borderWidth' | 'isInteractive' | 'tooltip' >, - BarHandlers { + BarInteractionHandlers { bar: ComputedBarDatum & { data: { value: number @@ -229,11 +231,23 @@ export type BarTooltipComponent< I extends BarIndex = string, > = FunctionComponent> -export type BarHandlers = { - onClick?: (datum: ComputedDatum & { color: string }, event: MouseEvent) => void - onMouseEnter?: (datum: ComputedDatum, event: MouseEvent) => void - onMouseLeave?: (datum: ComputedDatum, event: MouseEvent) => void -} +type BarEventMap = Pick< + EventMap, + | 'onMouseEnter' + | 'onMouseMove' + | 'onMouseLeave' + | 'onClick' + | 'onDoubleClick' + | 'onFocus' + | 'onBlur' + | 'onKeyDown' + | 'onWheel' + | 'onContextMenu' +> +export type BarInteractionHandlers< + D extends BarDatum = BarDatum, + I extends BarIndex = string, +> = InteractionHandlers, BarEventMap> export type BarCommonProps = { indexBy: PropertyAccessor @@ -301,14 +315,16 @@ interface BarSvgExtraProps = DataProps & Partial> & Partial> & - BarHandlers & + BarInteractionHandlers & SvgDefsAndFill> & Dimensions & MotionProps + export type ResponsiveBarSvgProps< D extends BarDatum = BarDatum, I extends BarIndex = string, > = WithChartRef>, SVGSVGElement> + export type BarSvgPropsWithDefaults< D extends BarDatum = BarDatum, I extends BarIndex = string, @@ -335,16 +351,22 @@ export type BarCanvasProps< > = DataProps & Partial> & Partial> & - BarHandlers & + BarInteractionHandlers & Dimensions + export type ResponsiveBarCanvasProps< D extends BarDatum = BarDatum, I extends BarIndex = string, > = WithChartRef>, HTMLCanvasElement> + export type BarCanvasPropsWithDefaults< D extends BarDatum = BarDatum, I extends BarIndex = string, -> = DataProps & BarCommonProps & BarCanvasExtraProps & BarHandlers & Dimensions +> = DataProps & + BarCommonProps & + BarCanvasExtraProps & + BarInteractionHandlers & + Dimensions export type BarAnnotationsProps = { annotations: readonly AnnotationMatcher>[] From 17a3b6d308aa8cc0c4dafbf470046c7c824ac6fe Mon Sep 17 00:00:00 2001 From: plouc Date: Sat, 24 May 2025 08:10:35 +0900 Subject: [PATCH 3/4] feat(bar): improve default props casting --- packages/bar/src/Bar.tsx | 9 ++++++--- packages/bar/src/BarCanvas.tsx | 3 ++- packages/bar/src/defaults.ts | 14 ++++++-------- packages/bar/src/types.ts | 6 ++++-- 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/packages/bar/src/Bar.tsx b/packages/bar/src/Bar.tsx index 111ef2bf5..163ab37a7 100644 --- a/packages/bar/src/Bar.tsx +++ b/packages/bar/src/Bar.tsx @@ -17,6 +17,8 @@ import { BarLegends } from './BarLegends' import { useBar } from './hooks' import { svgDefaultProps } from './defaults' import { + BarAnnotationMatcher, + BarBorderColor, BarComponent, BarCustomLayerProps, BarDatum, @@ -66,14 +68,15 @@ const InnerBar = ({ labelPosition = svgDefaultProps.labelPosition, labelOffset = svgDefaultProps.labelOffset, markers = svgDefaultProps.markers, - colorBy, colors, + colorBy, defs = svgDefaultProps.defs, + // @ts-expect-error the typings for SVG fill are not easy to get right. fill = svgDefaultProps.fill, borderRadius = svgDefaultProps.borderRadius, borderWidth = svgDefaultProps.borderWidth, - borderColor, - annotations = svgDefaultProps.annotations, + borderColor = svgDefaultProps.borderColor as BarBorderColor, + annotations = svgDefaultProps.annotations as BarAnnotationMatcher[], legendLabel, tooltipLabel, valueFormat, diff --git a/packages/bar/src/BarCanvas.tsx b/packages/bar/src/BarCanvas.tsx index 0033eb65e..6863c28d1 100644 --- a/packages/bar/src/BarCanvas.tsx +++ b/packages/bar/src/BarCanvas.tsx @@ -30,6 +30,7 @@ import { renderAxesToCanvas, renderGridLinesToCanvas } from '@nivo/axes' import { renderLegendToCanvas } from '@nivo/legends' import { useTooltip } from '@nivo/tooltip' import { + BarAnnotationMatcher, BarCanvasCustomLayerProps, BarCanvasLayer, BarCanvasProps, @@ -113,7 +114,7 @@ const InnerBarCanvas = [], legendLabel, tooltipLabel, valueFormat, diff --git a/packages/bar/src/defaults.ts b/packages/bar/src/defaults.ts index 40c9ad758..4e0e6ec8e 100644 --- a/packages/bar/src/defaults.ts +++ b/packages/bar/src/defaults.ts @@ -1,7 +1,5 @@ -import { InheritedColorConfig, OrdinalColorScaleConfig } from '@nivo/colors' import { BarCommonProps, - BarDatum, ComputedDatum, BarSvgPropsWithDefaults, BarCanvasPropsWithDefaults, @@ -13,8 +11,8 @@ import { renderBar } from './renderBar' export const commonDefaultProps: Omit = { indexBy: 'id', keys: ['value'], - groupMode: 'stacked' as const, - layout: 'vertical' as const, + groupMode: 'stacked', + layout: 'vertical', valueScale: { type: 'linear', nice: true, round: false }, indexScale: { type: 'band', round: false }, padding: 0.1, @@ -23,16 +21,16 @@ export const commonDefaultProps: Omit = { enableGridY: true, enableLabel: true, label: 'formattedValue', - labelPosition: 'middle' as const, + labelPosition: 'middle', labelOffset: 0, labelSkipWidth: 0, labelSkipHeight: 0, labelTextColor: { theme: 'labels.text.fill' }, - colorBy: 'id' as const, - colors: { scheme: 'nivo' } as OrdinalColorScaleConfig, + colorBy: 'id', + colors: { scheme: 'nivo' }, borderRadius: 0, borderWidth: 0, - borderColor: { from: 'color' } as InheritedColorConfig, + borderColor: { from: 'color' }, isInteractive: true, tooltip: BarTooltip, tooltipLabel: (datum: ComputedDatum) => `${datum.id} - ${datum.indexValue}`, diff --git a/packages/bar/src/types.ts b/packages/bar/src/types.ts index 372a51a40..d0730478a 100644 --- a/packages/bar/src/types.ts +++ b/packages/bar/src/types.ts @@ -33,6 +33,8 @@ export type BarIndex = string | number | Date export type BarValue = number | null // Default datum type for the bar chart. export type BarDatum = Record +export type BarBorderColor = InheritedColorConfig> +export type BarAnnotationMatcher = AnnotationMatcher> export interface DataProps { data: readonly D[] @@ -261,7 +263,7 @@ export type BarCommonProps enableGridY: boolean gridYValues?: GridValues - borderColor: InheritedColorConfig> + borderColor: BarBorderColor borderRadius: number borderWidth: number enableLabel: boolean @@ -369,6 +371,6 @@ export type BarCanvasPropsWithDefaults< Dimensions export type BarAnnotationsProps = { - annotations: readonly AnnotationMatcher>[] + annotations: readonly BarAnnotationMatcher[] bars: readonly ComputedBarDatum[] } From e04d8ba45c7763c8af7a77e62b460568e5bb2767 Mon Sep 17 00:00:00 2001 From: plouc Date: Sat, 24 May 2025 09:16:54 +0900 Subject: [PATCH 4/4] feat(bar): propagate the index value type in compute helpers --- packages/bar/src/Bar.tsx | 8 ++-- packages/bar/src/BarCanvas.tsx | 9 ++-- packages/bar/src/compute/common.ts | 21 ++++++---- packages/bar/src/compute/grouped.ts | 16 ++++---- packages/bar/src/compute/legends.ts | 21 +++++----- packages/bar/src/compute/stacked.ts | 34 +++++++-------- packages/bar/src/compute/totals.ts | 10 ++--- packages/bar/src/hooks.ts | 38 +++++++++++------ packages/bar/src/types.ts | 64 ++++++++++++++++++++--------- 9 files changed, 132 insertions(+), 89 deletions(-) diff --git a/packages/bar/src/Bar.tsx b/packages/bar/src/Bar.tsx index 163ab37a7..04be3a389 100644 --- a/packages/bar/src/Bar.tsx +++ b/packages/bar/src/Bar.tsx @@ -161,7 +161,7 @@ const InnerBar = ({ ) const transition = useTransition< - ComputedBarDatumWithValue, + ComputedBarDatumWithValue, { borderColor: string color: string @@ -173,7 +173,7 @@ const InnerBar = ({ opacity: number transform: string width: number - textAnchor: BarItemProps['style']['textAnchor'] + textAnchor: BarItemProps['style']['textAnchor'] } >(barsWithValue, { keys: bar => bar.key, @@ -297,7 +297,7 @@ const InnerBar = ({ if (layers.includes('annotations')) { layerById.annotations = ( - + key="annotations" bars={bars} annotations={annotations} /> ) } @@ -374,7 +374,7 @@ const InnerBar = ({ if (layers.includes('totals') && enableTotals) { layerById.totals = ( - key="totals" data={barTotals} springConfig={springConfig} diff --git a/packages/bar/src/BarCanvas.tsx b/packages/bar/src/BarCanvas.tsx index 6863c28d1..dc619117d 100644 --- a/packages/bar/src/BarCanvas.tsx +++ b/packages/bar/src/BarCanvas.tsx @@ -38,6 +38,7 @@ import { BarCommonProps, BarDatum, BarIndex, + BarLabel, BarTooltipComponent, ComputedBarDatum, } from './types' @@ -105,7 +106,7 @@ const InnerBarCanvas = [], renderBar = canvasDefaultProps.renderBar as unknown as BarCanvasRenderer, enableLabel = canvasDefaultProps.enableLabel, - label, + label = canvasDefaultProps.label as BarLabel, labelSkipWidth = canvasDefaultProps.labelSkipWidth, labelSkipHeight = canvasDefaultProps.labelSkipHeight, labelTextColor, @@ -116,7 +117,7 @@ const InnerBarCanvas = [], legendLabel, - tooltipLabel, + tooltipLabel = canvasDefaultProps.tooltipLabel as BarLabel, valueFormat, isInteractive = canvasDefaultProps.isInteractive, tooltip = canvasDefaultProps.tooltip as BarTooltipComponent, @@ -183,7 +184,7 @@ const InnerBarCanvas = >({ annotations: useAnnotations({ data: bars, annotations, @@ -200,7 +201,7 @@ const InnerBarCanvas = = useMemo( + const layerContext: BarCanvasCustomLayerProps = useMemo( () => ({ borderRadius, borderWidth, diff --git a/packages/bar/src/compute/common.ts b/packages/bar/src/compute/common.ts index 5804b43f0..3e57eef29 100644 --- a/packages/bar/src/compute/common.ts +++ b/packages/bar/src/compute/common.ts @@ -1,13 +1,13 @@ import { ScaleBandSpec, ScaleBand, computeScale } from '@nivo/scales' import { commonDefaultProps } from '../defaults' -import { BarCommonProps, BarDatum } from '../types' +import { BarCommonProps, BarDatum, BarIndex } from '../types' /** * Generates indexed scale. */ -export const getIndexScale = ( +export const getIndexScale = ( data: readonly D[], - getIndex: (datum: D) => string, + getIndex: (datum: D) => I, padding: number, indexScale: ScaleBandSpec, size: number, @@ -26,7 +26,10 @@ export const getIndexScale = ( /** * This method ensures all the provided keys exist in the entire series. */ -export const normalizeData = (data: readonly D[], keys: readonly string[]) => +export const normalizeData = ( + data: readonly D[], + keys: readonly string[] +) => data.map( item => ({ @@ -38,7 +41,7 @@ export const normalizeData = (data: readonly D[], keys: read }) as D ) -export const filterNullValues = (data: D) => +export const filterNullValues = (data: D) => Object.keys(data).reduce>((acc, key) => { if (data[key]) { acc[key] = data[key] @@ -57,11 +60,11 @@ export type BarLabelLayout = { /** * Compute the label position and alignment based on a given position and offset. */ -export function useComputeLabelLayout( - layout: BarCommonProps['layout'] = commonDefaultProps.layout, +export function useComputeLabelLayout( + layout: BarCommonProps['layout'] = commonDefaultProps.layout, reverse: boolean, - labelPosition: BarCommonProps['labelPosition'] = commonDefaultProps.labelPosition, - labelOffset: BarCommonProps['labelOffset'] = commonDefaultProps.labelOffset + labelPosition: BarCommonProps['labelPosition'] = commonDefaultProps.labelPosition, + labelOffset: BarCommonProps['labelOffset'] = commonDefaultProps.labelOffset ): (width: number, height: number) => BarLabelLayout { return (width: number, height: number) => { // If the chart is reversed, we want to make sure the offset is also reversed diff --git a/packages/bar/src/compute/grouped.ts b/packages/bar/src/compute/grouped.ts index 93afb2ec6..aeb5856db 100644 --- a/packages/bar/src/compute/grouped.ts +++ b/packages/bar/src/compute/grouped.ts @@ -1,7 +1,7 @@ import { Margin } from '@nivo/core' import { OrdinalColorScale } from '@nivo/colors' import { Scale, ScaleBand, computeScale } from '@nivo/scales' -import { BarDatum, BarSvgProps, ComputedBarDatum, ComputedDatum } from '../types' +import { BarDatum, BarIndex, BarSvgProps, ComputedBarDatum, ComputedDatum } from '../types' import { coerceValue, filterNullValues, getIndexScale, normalizeData } from './common' type Params = { @@ -151,9 +151,9 @@ const generateHorizontalGroupedBars = ( } /** - * Generates x/y scales & bars for grouped bar chart. + * Generates x/y scales and bars for grouped bar chart. */ -export const generateGroupedBars = ({ +export const generateGroupedBars = ({ layout, width, height, @@ -164,7 +164,7 @@ export const generateGroupedBars = ({ hiddenIds = [], ...props }: Pick< - Required>, + Required>, | 'data' | 'height' | 'valueScale' @@ -176,9 +176,9 @@ export const generateGroupedBars = ({ | 'width' > & { formatValue: (value: number) => string - getColor: OrdinalColorScale> - getIndex: (datum: D) => string - getTooltipLabel: (datum: ComputedDatum) => string + getColor: OrdinalColorScale> + getIndex: (datum: D) => I + getTooltipLabel: (datum: ComputedBarDatum) => string margin: Margin hiddenIds?: readonly (string | number)[] }) => { @@ -221,7 +221,7 @@ export const generateGroupedBars = ({ scale(0) ?? 0, ] as const - const bars: ComputedBarDatum[] = + const bars: ComputedBarDatum[] = bandwidth > 0 ? layout === 'vertical' ? generateVerticalGroupedBars(...params) diff --git a/packages/bar/src/compute/legends.ts b/packages/bar/src/compute/legends.ts index f4e83db36..b055b986f 100644 --- a/packages/bar/src/compute/legends.ts +++ b/packages/bar/src/compute/legends.ts @@ -1,5 +1,6 @@ import { BarDatum, + BarIndex, BarLegendProps, BarSvgProps, BarsWithHidden, @@ -40,10 +41,10 @@ export const getLegendDataForKeys = ( return data } -export const getLegendDataForIndexes = ( - bars: BarsWithHidden, - layout: NonNullable['layout']>, - getLegendLabel: (datum: LegendLabelDatum) => string +export const getLegendDataForIndexes = ( + bars: BarsWithHidden, + layout: NonNullable['layout']>, + getLegendLabel: (datum: LegendLabelDatum) => string ): LegendData[] => { const data = uniqBy( bars.map(bar => ({ @@ -62,7 +63,7 @@ export const getLegendDataForIndexes = ( return data } -export const getLegendData = ({ +export const getLegendData = ({ bars, direction, from, @@ -70,11 +71,11 @@ export const getLegendData = ({ layout, legendLabel, reverse, -}: Pick>, 'layout' | 'groupMode'> & { - bars: BarsWithHidden +}: Pick>, 'layout' | 'groupMode'> & { + bars: BarsWithHidden direction: BarLegendProps['direction'] from: BarLegendProps['dataFrom'] - legendLabel: BarSvgProps['legendLabel'] + legendLabel: BarSvgProps['legendLabel'] reverse: boolean }) => { const getLegendLabel = getPropertyAccessor( @@ -82,8 +83,8 @@ export const getLegendData = ({ ) if (from === 'indexes') { - return getLegendDataForIndexes(bars, layout, getLegendLabel) + return getLegendDataForIndexes(bars, layout, getLegendLabel) } - return getLegendDataForKeys(bars, layout, direction, groupMode, reverse, getLegendLabel) + return getLegendDataForKeys(bars, layout, direction, groupMode, reverse, getLegendLabel) } diff --git a/packages/bar/src/compute/stacked.ts b/packages/bar/src/compute/stacked.ts index 113515721..e8f0817e2 100644 --- a/packages/bar/src/compute/stacked.ts +++ b/packages/bar/src/compute/stacked.ts @@ -2,7 +2,7 @@ import { Margin } from '@nivo/core' import { OrdinalColorScale } from '@nivo/colors' import { Scale, ScaleBand, computeScale } from '@nivo/scales' import { Series, SeriesPoint, stack, stackOffsetDiverging } from 'd3-shape' -import { BarDatum, BarSvgProps, ComputedBarDatum, ComputedDatum } from '../types' +import { BarDatum, BarIndex, BarSvgProps, ComputedBarDatum, ComputedDatum } from '../types' import { coerceValue, filterNullValues, getIndexScale, normalizeData } from './common' type StackDatum = SeriesPoint @@ -26,9 +26,9 @@ const filterZerosIfLog = (array: number[], type: string) => type === 'log' ? array.filter(num => num !== 0) : array /** - * Generates x/y scales & bars for vertical stacked bar chart. + * Generates x/y scales and bars for vertical stacked bar chart. */ -const generateVerticalStackedBars = ( +const generateVerticalStackedBars = ( { formatValue, getColor, @@ -42,7 +42,7 @@ const generateVerticalStackedBars = ( }: Params, barWidth: number, reverse: boolean -): ComputedBarDatum[] => { +): ComputedBarDatum[] => { const getY = (d: StackDatum) => yScale(d[reverse ? 0 : 1]) const getHeight = (d: StackDatum, y: number) => (yScale(d[reverse ? 1 : 0]) ?? 0) - y @@ -85,9 +85,9 @@ const generateVerticalStackedBars = ( } /** - * Generates x/y scales & bars for horizontal stacked bar chart. + * Generates x/y scales and bars for horizontal stacked bar chart. */ -const generateHorizontalStackedBars = ( +const generateHorizontalStackedBars = ( { formatValue, getColor, @@ -101,11 +101,11 @@ const generateHorizontalStackedBars = ( }: Params, barHeight: number, reverse: boolean -): ComputedBarDatum[] => { +): ComputedBarDatum[] => { const getX = (d: StackDatum) => xScale(d[reverse ? 1 : 0]) const getWidth = (d: StackDatum, x: number) => (xScale(d[reverse ? 0 : 1]) ?? 0) - x - const bars: ComputedBarDatum[] = [] + const bars: ComputedBarDatum[] = [] stackedData.forEach(stackedDataItem => yScale.domain().forEach((index, i) => { const d = stackedDataItem[i] @@ -144,9 +144,9 @@ const generateHorizontalStackedBars = ( } /** - * Generates x/y scales & bars for stacked bar chart. + * Generates x/y scales and bars for stacked bar chart. */ -export const generateStackedBars = ({ +export const generateStackedBars = ({ data, layout, width, @@ -157,7 +157,7 @@ export const generateStackedBars = ({ hiddenIds = [], ...props }: Pick< - Required>, + Required>, | 'data' | 'height' | 'valueScale' @@ -169,14 +169,14 @@ export const generateStackedBars = ({ | 'width' > & { formatValue: (value: number) => string - getColor: OrdinalColorScale> - getIndex: (datum: RawDatum) => string - getTooltipLabel: (datum: ComputedDatum) => string + getColor: OrdinalColorScale> + getIndex: (datum: D) => I + getTooltipLabel: (datum: ComputedBarDatum) => string margin: Margin hiddenIds?: readonly (string | number)[] }) => { const keys = props.keys.filter(key => !hiddenIds.includes(key)) - const stackedData = stack().keys(keys).offset(stackOffsetDiverging)( + const stackedData = stack().keys(keys).offset(stackOffsetDiverging)( normalizeData(data, keys) ) @@ -210,12 +210,12 @@ export const generateStackedBars = ({ const innerPadding = props.innerPadding > 0 ? props.innerPadding : 0 const bandwidth = indexScale.bandwidth() const params = [ - { ...props, innerPadding, stackedData, xScale, yScale } as Params, + { ...props, innerPadding, stackedData, xScale, yScale } as Params, bandwidth, valueScale.reverse ?? false, ] as const - const bars: ComputedBarDatum[] = + const bars: ComputedBarDatum[] = bandwidth > 0 ? layout === 'vertical' ? generateVerticalStackedBars(...params) diff --git a/packages/bar/src/compute/totals.ts b/packages/bar/src/compute/totals.ts index 0d67400eb..690f840b8 100644 --- a/packages/bar/src/compute/totals.ts +++ b/packages/bar/src/compute/totals.ts @@ -1,6 +1,6 @@ import { AnyScale, ScaleBand } from '@nivo/scales' import { commonDefaultProps } from '../defaults' -import { BarCommonProps, BarDatum, ComputedBarDatum } from '../types' +import { BarCommonProps, BarDatum, BarIndex, ComputedBarDatum } from '../types' export interface BarTotalsData { key: string @@ -11,12 +11,12 @@ export interface BarTotalsData { animationOffset: number } -export const computeBarTotals = ( - bars: ComputedBarDatum[], +export const computeBarTotals = ( + bars: ComputedBarDatum[], xScale: ScaleBand | AnyScale, yScale: ScaleBand | AnyScale, - layout: BarCommonProps['layout'] = commonDefaultProps.layout, - groupMode: BarCommonProps['groupMode'] = commonDefaultProps.groupMode, + layout: BarCommonProps['layout'] = commonDefaultProps.layout, + groupMode: BarCommonProps['groupMode'] = commonDefaultProps.groupMode, totalsOffset: number, formatValue: (value: number) => string ) => { diff --git a/packages/bar/src/hooks.ts b/packages/bar/src/hooks.ts index dd7cd3db3..063e5ec06 100644 --- a/packages/bar/src/hooks.ts +++ b/packages/bar/src/hooks.ts @@ -10,21 +10,26 @@ import { LegendData, BarLegendProps, BarIndex, + BarIndexBy, + BarBorderColor, + BarLabel, + BarColors, + BarLabelTextColor, } from './types' import { commonDefaultProps } from './defaults' import { generateGroupedBars, generateStackedBars, getLegendData } from './compute' import { computeBarTotals } from './compute/totals' export const useBar = ({ - indexBy = commonDefaultProps.indexBy, + indexBy = commonDefaultProps.indexBy as BarIndexBy, keys = commonDefaultProps.keys, - label = commonDefaultProps.label, - tooltipLabel = commonDefaultProps.tooltipLabel, + label = commonDefaultProps.label as BarLabel, + tooltipLabel = commonDefaultProps.tooltipLabel as BarLabel, valueFormat, - colors = commonDefaultProps.colors, + colors = commonDefaultProps.colors as BarColors, colorBy = commonDefaultProps.colorBy, - borderColor = commonDefaultProps.borderColor, - labelTextColor = commonDefaultProps.labelTextColor, + borderColor = commonDefaultProps.borderColor as BarBorderColor, + labelTextColor = commonDefaultProps.labelTextColor as BarLabelTextColor, groupMode = commonDefaultProps.groupMode, layout = commonDefaultProps.layout, data, @@ -88,11 +93,11 @@ export const useBar = >(borderColor, theme) - const getLabelColor = useInheritedColor>(labelTextColor, theme) + const getBorderColor = useInheritedColor(borderColor, theme) + const getLabelColor = useInheritedColor(labelTextColor, theme) const generateBars = groupMode === 'grouped' ? generateGroupedBars : generateStackedBars - const { bars, xScale, yScale } = generateBars({ + const { bars, xScale, yScale } = generateBars({ layout, data, getIndex, @@ -113,7 +118,7 @@ export const useBar = bars - .filter((bar): bar is ComputedBarDatumWithValue => bar.data.value !== null) + .filter((bar): bar is ComputedBarDatumWithValue => bar.data.value !== null) .map((bar, index) => ({ ...bar, index, @@ -146,7 +151,7 @@ export const useBar = legends.map(legend => { - const data = getLegendData({ + const data = getLegendData({ bars: legend.dataFrom === 'keys' ? legendData : bars, direction: legend.direction, from: legend.dataFrom, @@ -162,7 +167,16 @@ export const useBar = computeBarTotals(bars, xScale, yScale, layout, groupMode, totalsOffset, formatValue), + () => + computeBarTotals( + bars, + xScale, + yScale, + layout, + groupMode, + totalsOffset, + formatValue + ), [bars, xScale, yScale, layout, groupMode, totalsOffset, formatValue] ) diff --git a/packages/bar/src/types.ts b/packages/bar/src/types.ts index d0730478a..62bef6c6c 100644 --- a/packages/bar/src/types.ts +++ b/packages/bar/src/types.ts @@ -33,8 +33,30 @@ export type BarIndex = string | number | Date export type BarValue = number | null // Default datum type for the bar chart. export type BarDatum = Record -export type BarBorderColor = InheritedColorConfig> -export type BarAnnotationMatcher = AnnotationMatcher> +export type BarIndexBy< + D extends BarDatum = BarDatum, + I extends BarIndex = string, +> = PropertyAccessor +export type BarColors< + D extends BarDatum = BarDatum, + I extends BarIndex = string, +> = OrdinalColorScaleConfig> +export type BarBorderColor< + D extends BarDatum = BarDatum, + I extends BarIndex = string, +> = InheritedColorConfig> +export type BarLabel = PropertyAccessor< + ComputedBarDatum, + string +> +export type BarLabelTextColor< + D extends BarDatum = BarDatum, + I extends BarIndex = string, +> = InheritedColorConfig> +export type BarAnnotationMatcher< + D extends BarDatum = BarDatum, + I extends BarIndex = string, +> = AnnotationMatcher> export interface DataProps { data: readonly D[] @@ -74,20 +96,22 @@ export type ComputedBarDatum = Array< - Partial<{ - key: string - x: number - y: number - width: number - height: number - color: string - }> & { - data: Partial> & { - id: string | number - hidden: boolean +export type BarsWithHidden = Readonly< + Array< + Partial<{ + key: string + x: number + y: number + width: number + height: number + color: string + }> & { + data: Partial> & { + id: string | number + hidden: boolean + } } - } + > > export type LegendLabelDatum = Partial< @@ -252,7 +276,7 @@ export type BarInteractionHandlers< > = InteractionHandlers, BarEventMap> export type BarCommonProps = { - indexBy: PropertyAccessor + indexBy: BarIndexBy keys: readonly string[] margin?: Box innerPadding: number @@ -267,22 +291,22 @@ export type BarCommonProps, string> + label: BarLabel labelPosition: 'start' | 'middle' | 'end' labelOffset: number labelFormat?: string | LabelFormatter labelSkipWidth: number labelSkipHeight: number - labelTextColor: InheritedColorConfig> + labelTextColor: BarLabelTextColor isInteractive: boolean tooltip: BarTooltipComponent valueFormat?: ValueFormat legendLabel?: PropertyAccessor, string> - tooltipLabel: PropertyAccessor, string> + tooltipLabel: BarLabel groupMode: 'grouped' | 'stacked' layout: 'horizontal' | 'vertical' colorBy: 'id' | 'indexValue' - colors: OrdinalColorScaleConfig> + colors: BarColors theme: PartialTheme annotations: readonly AnnotationMatcher>[] legends: readonly BarLegendProps[]