diff --git a/packages/bar/index.d.ts b/packages/bar/index.d.ts deleted file mode 100644 index d88c8011c..000000000 --- a/packages/bar/index.d.ts +++ /dev/null @@ -1,172 +0,0 @@ -/* - * This file is part of the nivo project. - * - * Copyright 2016-present, Raphaël Benitte. - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -import * as React from 'react' -import { - Dimensions, - Box, - Theme, - MotionProps, - SvgDefsAndFill, - CartesianMarkerProps, -} from '@nivo/core' -import { AxisProps, GridValues } from '@nivo/axes' -import { OrdinalColorScaleConfig, InheritedColorConfig } from '@nivo/colors' -import { LegendProps } from '@nivo/legends' -import { ScaleSpec, ScaleBandSpec } from '@nivo/scales' - -declare module '@nivo/bar' { - export type Value = string | number - - export interface Data { - data: Array> - } - - export interface BarDatum { - [key: string]: Value - } - - export type BarDatumWithColor = BarDatum & { - color: string - } - - export interface BarExtendedDatum { - id: Value - value: number - index: number - indexValue: Value - data: BarDatum - } - - export type AccessorFunc = (datum: BarDatum) => string - - export type IndexByFunc = (datum: BarDatum) => string | number - - export type LabelFormatter = (label: string | number) => string | number - - export type ValueFormatter = (value: number) => string | number - - type GraphicsContainer = HTMLCanvasElement | SVGElement - - export type BarMouseEventHandler = ( - datum: BarExtendedDatum, - event: React.MouseEvent - ) => void - - export type BarTooltipDatum = BarExtendedDatum & { color: string } - export type TooltipProp = React.FC - - export interface BarItemProps { - data: BarExtendedDatum - x: number - y: number - width: number - height: number - color: string - borderRadius: number - borderWidth: number - borderColor: string - label: string - shouldRenderLabel: boolean - labelColor: string - onClick: BarMouseEventHandler - onMouseEnter: BarMouseEventHandler - onMouseLeave: BarMouseEventHandler - tooltipFormat: string | ValueFormatter - tooltip: TooltipProp - showTooltip: (tooltip: React.ReactNode, event: React.MouseEvent) => void - hideTooltip: () => void - getTooltipLabel: (data: BarExtendedDatum) => React.ReactNode - theme: Theme - } - - export type BarProps = Partial<{ - indexBy: string | IndexByFunc - keys: string[] - - groupMode: 'stacked' | 'grouped' - layout: 'horizontal' | 'vertical' - reverse: boolean - - innerPadding: number - minValue: number | 'auto' - margin: Box - maxValue: number | 'auto' - padding: number - - valueScale: ScaleSpec - indexScale: ScaleBandSpec - - axisBottom: AxisProps | null - axisLeft: AxisProps | null - axisRight: AxisProps | null - axisTop: AxisProps | null - - enableGridX: boolean - gridXValues: GridValues - enableGridY: boolean - gridYValues: GridValues - - barComponent: React.FC - - enableLabel: boolean - label: string | AccessorFunc - labelFormat: string | LabelFormatter - labelLinkColor: InheritedColorConfig - labelSkipWidth: number - labelSkipHeight: number - labelTextColor: InheritedColorConfig - - colors: OrdinalColorScaleConfig - borderColor: InheritedColorConfig - borderRadius: number - borderWidth: number - theme: Theme - - isInteractive: boolean - tooltipFormat: string | ValueFormatter - tooltip: TooltipProp - - legends: ({ dataFrom: 'indexes' | 'keys' } & LegendProps)[] - - markers: CartesianMarkerProps[] - - renderWrapper: boolean - }> - - export type BarLayerType = 'grid' | 'axes' | 'bars' | 'markers' | 'legends' - export type BarCustomLayer = (props: any) => React.ReactNode - export type Layer = BarLayerType | BarCustomLayer - - export type BarSvgProps = Data & - BarProps & - MotionProps & - SvgDefsAndFill & - Partial<{ - layers: Layer[] - onClick: BarMouseEventHandler - onMouseEnter: BarMouseEventHandler - onMouseLeave: BarMouseEventHandler - role: string - }> - - export class Bar extends React.Component {} - export class ResponsiveBar extends React.Component {} - - export type BarCanvasProps = Data & - BarProps & - Partial<{ - onClick: BarMouseEventHandler - onMouseEnter: BarMouseEventHandler - onMouseLeave: BarMouseEventHandler - pixelRatio: number - }> - - export class BarCanvas extends React.Component {} - export class ResponsiveBarCanvas extends React.Component {} -} diff --git a/packages/bar/package.json b/packages/bar/package.json index baccea0ca..85966b2a2 100644 --- a/packages/bar/package.json +++ b/packages/bar/package.json @@ -21,11 +21,12 @@ ], "main": "./dist/nivo-bar.cjs.js", "module": "./dist/nivo-bar.es.js", + "typings": "./dist/types/index.d.ts", "files": [ "README.md", "LICENSE.md", - "index.d.ts", - "dist/" + "dist/", + "!dist/tsconfig.tsbuildinfo" ], "dependencies": { "@nivo/annotations": "0.71.0", @@ -45,7 +46,6 @@ }, "peerDependencies": { "@nivo/core": "0.71.0", - "prop-types": ">= 15.5.10 < 16.0.0", "react": ">= 16.8.4 < 18.0.0" }, "publishConfig": { diff --git a/packages/bar/src/Bar.js b/packages/bar/src/Bar.tsx similarity index 64% rename from packages/bar/src/Bar.js rename to packages/bar/src/Bar.tsx index c94385eb6..022666f44 100644 --- a/packages/bar/src/Bar.js +++ b/packages/bar/src/Bar.tsx @@ -1,128 +1,146 @@ -/* - * This file is part of the nivo project. - * - * Copyright 2016-present, Raphaël Benitte. - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ import { createElement, Fragment, useCallback, useState } from 'react' import { TransitionMotion, spring } from 'react-motion' -import { bindDefs, LegacyContainer, SvgWrapper, CartesianMarkers } from '@nivo/core' +import { + // @ts-ignore + bindDefs, + // @ts-ignore + LegacyContainer, + SvgWrapper, + // @ts-ignore + CartesianMarkers, + defaultMargin, + usePropertyAccessor, +} from '@nivo/core' import { Axes, Grid } from '@nivo/axes' +import { BarAnnotations } from './BarAnnotations' +import { BarDatum, BarLayer, BarSvgProps, ComputedBarDatum, TooltipHandlers } from './types' import { BoxLegendSvg } from '@nivo/legends' -import { setDisplayName } from '@nivo/recompose' import { generateGroupedBars, generateStackedBars, getLegendData } from './compute' -import enhance from './enhance' -import { BarSvgDefaultProps, BarSvgPropTypes } from './props' -import BarAnnotations from './BarAnnotations' +import { svgDefaultProps } from './props' +import { useInheritedColor, useOrdinalColorScale } from '@nivo/colors' -const barWillEnterHorizontal = ({ style }) => ({ +type SpringConfig = Required<{ + damping: BarSvgProps['motionDamping'] + stiffness: BarSvgProps['motionStiffness'] +}> + +const barWillEnterHorizontal = ({ style }: Record) => ({ x: style.x.val, y: style.y.val, width: 0, height: style.height.val, }) -const barWillEnterVertical = ({ style }) => ({ +const barWillEnterVertical = ({ style }: Record) => ({ x: style.x.val, y: style.y.val + style.height.val, width: style.width.val, height: 0, }) -const barWillLeaveHorizontal = springConfig => ({ style }) => ({ +const barWillLeaveHorizontal = (springConfig: SpringConfig) => ({ + style, +}: Record) => ({ x: style.x, y: style.y, width: spring(0, springConfig), height: style.height, }) -const barWillLeaveVertical = springConfig => ({ style }) => ({ +const barWillLeaveVertical = (springConfig: SpringConfig) => ({ style }: Record) => ({ x: style.x, y: spring(style.y.val + style.height.val, springConfig), width: style.width, height: spring(0, springConfig), }) -const Bar = props => { - const { - data, - getIndex, - keys, - - groupMode, - layout, - reverse, - minValue, - maxValue, - - valueScale, - indexScale, - - margin, - width, - height, - outerWidth, - outerHeight, - padding, - innerPadding, - - axisTop, - axisRight, - axisBottom, - axisLeft, - enableGridX, - enableGridY, - gridXValues, - gridYValues, - - layers, - barComponent, - - enableLabel, - getLabel, - labelSkipWidth, - labelSkipHeight, - getLabelTextColor, - - markers, - - theme, - getColor, - defs, - fill, - borderRadius, - borderWidth, - getBorderColor, - - annotations, - - isInteractive, - getTooltipLabel, - tooltipFormat, - tooltip, - onClick, - onMouseEnter, - onMouseLeave, - - legends, - - animate, - motionStiffness, - motionDamping, - - renderWrapper, - role, - } = props - - const [hiddenIds, setHiddenIds] = useState([]) +export const Bar = ({ + data, + indexBy = svgDefaultProps.indexBy, + keys = svgDefaultProps.keys, + + groupMode = svgDefaultProps.groupMode, + layout = svgDefaultProps.layout, + reverse = svgDefaultProps.reverse, + minValue = svgDefaultProps.minValue, + maxValue = svgDefaultProps.maxValue, + + valueScale = svgDefaultProps.valueScale, + indexScale = svgDefaultProps.indexScale, + + padding = svgDefaultProps.padding, + innerPadding = svgDefaultProps.innerPadding, + + axisTop, + axisRight, + axisBottom = svgDefaultProps.axisBottom, + axisLeft = svgDefaultProps.axisLeft, + enableGridX = svgDefaultProps.enableGridX, + enableGridY = svgDefaultProps.enableGridY, + gridXValues, + gridYValues, + + layers = svgDefaultProps.layers as BarLayer[], + barComponent = svgDefaultProps.barComponent, + + enableLabel = svgDefaultProps.enableLabel, + label = svgDefaultProps.label, + labelSkipWidth = svgDefaultProps.labelSkipWidth, + labelSkipHeight = svgDefaultProps.labelSkipHeight, + labelTextColor = svgDefaultProps.labelTextColor, + + markers, + + theme, + colorBy = svgDefaultProps.colorBy, + colors = svgDefaultProps.colors, + defs = svgDefaultProps.defs, + fill = svgDefaultProps.fill, + borderRadius = svgDefaultProps.borderRadius, + borderWidth = svgDefaultProps.borderWidth, + borderColor = svgDefaultProps.borderColor, + + annotations = svgDefaultProps.annotations, + + isInteractive = svgDefaultProps.isInteractive, + tooltipLabel = svgDefaultProps.tooltipLabel, + tooltipFormat, + tooltip = svgDefaultProps.tooltip, + onClick, + onMouseEnter, + onMouseLeave, + + legends = svgDefaultProps.legends, + + animate = svgDefaultProps.animate, + motionStiffness = svgDefaultProps.motionStiffness, + motionDamping = svgDefaultProps.motionDamping, + + renderWrapper, + role = svgDefaultProps.role, + + ...props +}: BarSvgProps) => { + const [hiddenIds, setHiddenIds] = useState([]) const toggleSerie = useCallback(id => { setHiddenIds(state => state.indexOf(id) > -1 ? state.filter(item => item !== id) : [...state, id] ) }, []) + const margin = { ...defaultMargin, ...props.margin } + const outerHeight = props.height + const outerWidth = props.width + const height = props.height - margin.top - margin.bottom + const width = props.width - margin.left - margin.right + + const getBorderColor = useInheritedColor>(borderColor, theme) + const getColor = useOrdinalColorScale(colors, colorBy) + const getIndex = usePropertyAccessor(indexBy) + const getLabel = usePropertyAccessor(label) + const getLabelColor = useInheritedColor>(labelTextColor, theme) + const getTooltipLabel = usePropertyAccessor(tooltipLabel) + const generateBars = groupMode === 'grouped' ? generateGroupedBars : generateStackedBars const result = generateBars({ layout, @@ -142,12 +160,6 @@ const Bar = props => { hiddenIds, }) - const motionProps = { - animate, - motionDamping, - motionStiffness, - } - const springConfig = { damping: motionDamping, stiffness: motionStiffness, @@ -159,12 +171,15 @@ const Bar = props => { ? barWillLeaveVertical(springConfig) : barWillLeaveHorizontal(springConfig) - const shouldRenderLabel = ({ width, height }) => { - if (!enableLabel) return false - if (labelSkipWidth > 0 && width < labelSkipWidth) return false - if (labelSkipHeight > 0 && height < labelSkipHeight) return false - return true - } + const shouldRenderLabel = useCallback( + ({ width, height }: { height: number; width: number }) => { + if (!enableLabel) return false + if (labelSkipWidth > 0 && width < labelSkipWidth) return false + if (labelSkipHeight > 0 && height < labelSkipHeight) return false + return true + }, + [enableLabel, labelSkipHeight, labelSkipWidth] + ) const boundDefs = bindDefs(defs, result.bars, fill, { dataKey: 'data', @@ -175,7 +190,7 @@ const Bar = props => { - {({ showTooltip, hideTooltip }) => { + {({ showTooltip, hideTooltip }: TooltipHandlers) => { const commonProps = { borderRadius, borderWidth, @@ -187,7 +202,6 @@ const Bar = props => { onClick, onMouseEnter, onMouseLeave, - theme, getTooltipLabel, tooltipFormat, tooltip, @@ -226,9 +240,9 @@ const Bar = props => { width: Math.max(style.width, 0), height: Math.max(style.height, 0), label: getLabel(bar.data), - labelColor: getLabelTextColor(baseProps, theme), + // @ts-ignore fix theme + labelColor: getLabelColor(baseProps, theme), borderColor: getBorderColor(baseProps), - theme, }) })} @@ -240,14 +254,13 @@ const Bar = props => { .filter(bar => bar.data.value !== null) .map(d => createElement(barComponent, { - key: d.key, ...d, ...commonProps, label: getLabel(d.data), shouldRenderLabel: shouldRenderLabel(d), - labelColor: getLabelTextColor(d, theme), + // @ts-ignore fix theme + labelColor: getLabelColor(d, theme), borderColor: getBorderColor(d), - theme, }) ) } @@ -258,8 +271,8 @@ const Bar = props => { key="grid" width={width} height={height} - xScale={enableGridX ? result.xScale : null} - yScale={enableGridY ? result.yScale : null} + xScale={enableGridX ? (result.xScale as any) : null} + yScale={enableGridY ? (result.yScale as any) : null} xValues={gridXValues} yValues={gridYValues} /> @@ -267,8 +280,8 @@ const Bar = props => { axes: ( { containerWidth={width} containerHeight={height} data={legendData} - theme={theme} toggleSerie={legend.toggleSerie ? toggleSerie : undefined} /> ) @@ -316,11 +328,8 @@ const Bar = props => { annotations: ( ), } @@ -331,14 +340,13 @@ const Bar = props => { height={outerHeight} margin={margin} defs={boundDefs} - theme={theme} role={role} > {layers.map((layer, i) => { if (typeof layer === 'function') { return ( - {layer({ ...props, ...result, showTooltip, hideTooltip })} + {layer({ ...commonProps, ...result } as any)} ) } @@ -350,8 +358,3 @@ const Bar = props => { ) } - -Bar.propTypes = BarSvgPropTypes -Bar.defaultProps = BarSvgDefaultProps - -export default setDisplayName('Bar')(enhance(Bar)) diff --git a/packages/bar/src/BarAnnotations.js b/packages/bar/src/BarAnnotations.js deleted file mode 100644 index fd7f82db5..000000000 --- a/packages/bar/src/BarAnnotations.js +++ /dev/null @@ -1,54 +0,0 @@ -/* - * This file is part of the nivo project. - * - * Copyright 2016-present, Raphaël Benitte. - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -import { Annotation, useAnnotations } from '@nivo/annotations' - -const BarAnnotations = ({ - bars, - annotations, - animate, - innerWidth, - innerHeight, - motionStiffness, - motionDamping, -}) => { - const boundAnnotations = useAnnotations({ - items: bars, - annotations, - getPosition: bar => ({ - x: bar.x + bar.width / 2, - y: bar.y + bar.height / 2, - }), - getDimensions: (bar, offset) => { - const width = bar.width + offset * 2 - const height = bar.height + offset * 2 - - return { - width, - height, - size: Math.max(width, height), - } - }, - }) - - return boundAnnotations.map((annotation, i) => ( - - )) -} - -BarAnnotations.propTypes = {} - -export default BarAnnotations diff --git a/packages/bar/src/BarAnnotations.tsx b/packages/bar/src/BarAnnotations.tsx new file mode 100644 index 000000000..f25ef915d --- /dev/null +++ b/packages/bar/src/BarAnnotations.tsx @@ -0,0 +1,31 @@ +import { Annotation, useAnnotations } from '@nivo/annotations' +import { BarAnnotationsProps } from './types' + +export const BarAnnotations = ({ bars, annotations }: BarAnnotationsProps) => { + const boundAnnotations = useAnnotations({ + data: bars, + annotations, + getPosition: bar => ({ + x: bar.x + bar.width / 2, + y: bar.y + bar.height / 2, + }), + getDimensions: bar => { + const width = bar.width + const height = bar.height + + return { + width, + height, + size: Math.max(width, height), + } + }, + }) + + return ( + <> + {boundAnnotations.map((annotation, i) => ( + + ))} + + ) +} diff --git a/packages/bar/src/BarCanvas.js b/packages/bar/src/BarCanvas.tsx similarity index 87% rename from packages/bar/src/BarCanvas.js rename to packages/bar/src/BarCanvas.tsx index 32b188c6b..f86217577 100644 --- a/packages/bar/src/BarCanvas.js +++ b/packages/bar/src/BarCanvas.tsx @@ -1,28 +1,32 @@ -/* - * This file is part of the nivo project. - * - * Copyright 2016-present, Raphaël Benitte. - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -import { forwardRef, Component } from 'react' +// @ts-nocheck +import { BarCanvasProps, BarDatum, TooltipHandlers } from './types' +import { forwardRef, Component, ForwardedRef } from 'react' import uniqBy from 'lodash/uniqBy' +// @ts-ignore LegacyContainer import { getRelativeCursor, isCursorInRect, LegacyContainer } from '@nivo/core' import { renderAxesToCanvas, renderGridLinesToCanvas } from '@nivo/axes' import { renderLegendToCanvas } from '@nivo/legends' -import { setDisplayName } from '@nivo/recompose' import { BasicTooltip } from '@nivo/tooltip' import { generateGroupedBars, generateStackedBars } from './compute' -import { BarDefaultProps, BarPropTypes } from './props' -import enhance from './enhance' +// import { canvasDefaultProps } from './props' + +declare module 'react' { + // eslint-disable-next-line @typescript-eslint/ban-types + function forwardRef( + render: (props: P, ref: React.Ref) => React.ReactElement | null + ): (props: P & React.RefAttributes) => React.ReactElement | null +} + +type InnerBarCanvasProps = BarCanvasProps & { + canvasRef: ForwardedRef +} const findNodeUnderCursor = (nodes, margin, x, y) => nodes.find(node => isCursorInRect(node.x + margin.left, node.y + margin.top, node.width, node.height, x, y) ) -class BarCanvas extends Component { +class InnerBarCanvas extends Component> { componentDidMount() { this.ctx = this.surface.getContext('2d') this.draw(this.props) @@ -109,6 +113,7 @@ class BarCanvas extends Component { innerPadding, valueScale, indexScale, + hiddenIds: [], } const result = @@ -251,7 +256,7 @@ class BarCanvas extends Component { hideTooltip() } - handleClick = event => { + handleClick = (event: React.MouseEvent) => { if (!this.bars) return const { margin, onClick } = this.props @@ -263,8 +268,8 @@ class BarCanvas extends Component { render() { const { - outerWidth, - outerHeight, + width: outerWidth, + height: outerHeight, pixelRatio, isInteractive, renderWrapper, @@ -274,7 +279,7 @@ class BarCanvas extends Component { return ( - {({ showTooltip, hideTooltip }) => ( + {({ showTooltip, hideTooltip }: TooltipHandlers) => ( { this.surface = surface @@ -297,8 +302,9 @@ class BarCanvas extends Component { } } -BarCanvas.propTypes = BarPropTypes -BarCanvas.defaultProps = BarDefaultProps - -const EnhancedBarCanvas = setDisplayName('BarCanvas')(enhance(BarCanvas)) -export default forwardRef((props, ref) => ) +export const BarCanvas = forwardRef( + ( + props: BarCanvasProps, + ref: ForwardedRef + ) => +) diff --git a/packages/bar/src/BarItem.js b/packages/bar/src/BarItem.js deleted file mode 100644 index 48acd9663..000000000 --- a/packages/bar/src/BarItem.js +++ /dev/null @@ -1,131 +0,0 @@ -/* - * This file is part of the nivo project. - * - * Copyright 2016-present, Raphaël Benitte. - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -import { compose, pure, withPropsOnChange } from '@nivo/recompose' -import { createElement } from 'react' -import PropTypes from 'prop-types' - -const BarItem = ({ - data, - - x, - y, - width, - height, - borderRadius, - color, - borderWidth, - borderColor, - - label, - shouldRenderLabel, - labelColor, - - showTooltip, - hideTooltip, - onClick, - onMouseEnter, - onMouseLeave, - - getTooltipLabel, - tooltip, - tooltipFormat, - - theme, -}) => { - const handleTooltip = e => - showTooltip(createElement(tooltip, { ...data, color, getTooltipLabel, tooltipFormat }), e) - const handleMouseEnter = e => { - onMouseEnter(data, e) - showTooltip(createElement(tooltip, { ...data, color, getTooltipLabel, tooltipFormat }), e) - } - const handleMouseLeave = e => { - onMouseLeave(data, e) - hideTooltip(e) - } - - return ( - - - {shouldRenderLabel && ( - - {label} - - )} - - ) -} - -BarItem.propTypes = { - data: PropTypes.shape({ - id: PropTypes.string.isRequired, - value: PropTypes.number.isRequired, - indexValue: PropTypes.string.isRequired, - fill: PropTypes.string, - }).isRequired, - - x: PropTypes.number.isRequired, - y: PropTypes.number.isRequired, - width: PropTypes.number.isRequired, - height: PropTypes.number.isRequired, - color: PropTypes.string.isRequired, - borderRadius: PropTypes.number.isRequired, - borderWidth: PropTypes.number.isRequired, - borderColor: PropTypes.string.isRequired, - - label: PropTypes.node.isRequired, - shouldRenderLabel: PropTypes.bool.isRequired, - labelColor: PropTypes.string.isRequired, - - showTooltip: PropTypes.func.isRequired, - hideTooltip: PropTypes.func.isRequired, - getTooltipLabel: PropTypes.func.isRequired, - tooltipFormat: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), - onClick: PropTypes.func, - onMouseEnter: PropTypes.func, - onMouseLeave: PropTypes.func, - tooltip: PropTypes.oneOfType([PropTypes.element, PropTypes.func]).isRequired, - - theme: PropTypes.shape({ - tooltip: PropTypes.shape({}).isRequired, - labels: PropTypes.shape({ - text: PropTypes.object.isRequired, - }).isRequired, - }).isRequired, -} - -const enhance = compose( - withPropsOnChange(['data', 'color', 'onClick'], ({ data, color, onClick }) => ({ - onClick: event => onClick({ color, ...data }, event), - })), - pure -) - -export default enhance(BarItem) diff --git a/packages/bar/src/BarItem.tsx b/packages/bar/src/BarItem.tsx new file mode 100644 index 000000000..4552e1679 --- /dev/null +++ b/packages/bar/src/BarItem.tsx @@ -0,0 +1,98 @@ +import { BarDatum, BarItemProps } from './types' +import { createElement, useCallback } from 'react' +import { useTheme } from '@nivo/core' + +export const BarItem = ({ + data, + + x, + y, + width, + height, + borderRadius, + color, + borderWidth, + borderColor, + + label, + shouldRenderLabel, + labelColor, + + showTooltip, + hideTooltip, + onClick, + onMouseEnter, + onMouseLeave, + + getTooltipLabel, + tooltip, + tooltipFormat, +}: BarItemProps) => { + const theme = useTheme() + + const handleClick = useCallback( + (event: React.MouseEvent) => { + onClick?.({ color, ...data }, event) + }, + [color, data, onClick] + ) + + const handleTooltip = useCallback( + (event: React.MouseEvent) => + showTooltip( + createElement(tooltip, { ...data, color, getTooltipLabel, tooltipFormat }), + event + ), + [color, data, getTooltipLabel, showTooltip, tooltip, tooltipFormat] + ) + const handleMouseEnter = useCallback( + (event: React.MouseEvent) => { + onMouseEnter?.(data, event) + showTooltip( + createElement(tooltip, { ...data, color, getTooltipLabel, tooltipFormat }), + event + ) + }, + [color, data, getTooltipLabel, onMouseEnter, showTooltip, tooltip, tooltipFormat] + ) + const handleMouseLeave = useCallback( + (event: React.MouseEvent) => { + onMouseLeave?.(data, event) + hideTooltip() + }, + [data, hideTooltip, onMouseLeave] + ) + + return ( + + + {shouldRenderLabel && ( + + {label} + + )} + + ) +} diff --git a/packages/bar/src/BarTooltip.js b/packages/bar/src/BarTooltip.tsx similarity index 59% rename from packages/bar/src/BarTooltip.js rename to packages/bar/src/BarTooltip.tsx index 2acce0742..831844751 100644 --- a/packages/bar/src/BarTooltip.js +++ b/packages/bar/src/BarTooltip.tsx @@ -1,6 +1,12 @@ +import { BarTooltipProps } from './types' import { BasicTooltip } from '@nivo/tooltip' -const BarTooltip = ({ color, getTooltipLabel, tooltipFormat, ...data }) => { +export const BarTooltip = ({ + color, + getTooltipLabel, + tooltipFormat, + ...data +}: BarTooltipProps) => { return ( { /> ) } - -export default BarTooltip diff --git a/packages/bar/src/ResponsiveBar.js b/packages/bar/src/ResponsiveBar.js deleted file mode 100644 index 4943ccd7e..000000000 --- a/packages/bar/src/ResponsiveBar.js +++ /dev/null @@ -1,18 +0,0 @@ -/* - * This file is part of the nivo project. - * - * Copyright 2016-present, Raphaël Benitte. - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -import { ResponsiveWrapper } from '@nivo/core' -import Bar from './Bar' - -const ResponsiveBar = props => ( - - {({ width, height }) => } - -) - -export default ResponsiveBar diff --git a/packages/bar/src/ResponsiveBar.tsx b/packages/bar/src/ResponsiveBar.tsx new file mode 100644 index 000000000..20f6fa989 --- /dev/null +++ b/packages/bar/src/ResponsiveBar.tsx @@ -0,0 +1,11 @@ +import { Bar } from './Bar' +import { BarDatum, BarSvgProps } from './types' +import { ResponsiveWrapper } from '@nivo/core' + +export const ResponsiveBar = ( + props: Omit, 'height' | 'width'> +) => ( + + {({ width, height }) => } + +) diff --git a/packages/bar/src/ResponsiveBarCanvas.js b/packages/bar/src/ResponsiveBarCanvas.js deleted file mode 100644 index e2b8f2026..000000000 --- a/packages/bar/src/ResponsiveBarCanvas.js +++ /dev/null @@ -1,19 +0,0 @@ -/* - * This file is part of the nivo project. - * - * Copyright 2016-present, Raphaël Benitte. - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -import { forwardRef } from 'react' -import { ResponsiveWrapper } from '@nivo/core' -import BarCanvas from './BarCanvas' - -const ResponsiveBarCanvas = (props, ref) => ( - - {({ width, height }) => } - -) - -export default forwardRef(ResponsiveBarCanvas) diff --git a/packages/bar/src/ResponsiveBarCanvas.tsx b/packages/bar/src/ResponsiveBarCanvas.tsx new file mode 100644 index 000000000..3bcffa8d5 --- /dev/null +++ b/packages/bar/src/ResponsiveBarCanvas.tsx @@ -0,0 +1,16 @@ +import { BarDatum, BarCanvasProps } from './types' +import { BarCanvas } from './BarCanvas' +import { ForwardedRef, forwardRef } from 'react' +import { ResponsiveWrapper } from '@nivo/core' + +export const ResponsiveBarCanvas = forwardRef(function ResponsiveBarCanvas< + RawDatum extends BarDatum +>(props: Omit, 'height' | 'width'>, ref: ForwardedRef) { + return ( + + {({ width, height }) => ( + + )} + + ) +}) diff --git a/packages/bar/src/compute/common.js b/packages/bar/src/compute/common.js deleted file mode 100644 index cfee25940..000000000 --- a/packages/bar/src/compute/common.js +++ /dev/null @@ -1,46 +0,0 @@ -/* - * This file is part of the nivo project. - * - * Copyright 2016-present, Raphaël Benitte. - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -import { computeScale } from '@nivo/scales' - -/** - * Generates indexed scale. - * - * @param {Array.} data - * @param {Function} getIndex - * @param {number} padding - * @Param {scalePropType} indexScale - * @Param {number} size - * @Param {'x' | 'y'} axis - * @returns {Function} - */ -export const getIndexScale = (data, getIndex, padding, indexScale, size, axis) => { - return computeScale( - indexScale, - { all: data.map(getIndex), min: 0, max: 0 }, - size, - axis - ).padding(padding) -} - -export const normalizeData = (data, keys) => - data.map(item => ({ - ...keys.reduce((acc, key) => { - acc[key] = null - return acc - }, {}), - ...item, - })) - -export const filterNullValues = data => - Object.keys(data).reduce((acc, key) => { - if (data[key]) { - acc[key] = data[key] - } - return acc - }, {}) diff --git a/packages/bar/src/compute/common.ts b/packages/bar/src/compute/common.ts new file mode 100644 index 000000000..d313c5dca --- /dev/null +++ b/packages/bar/src/compute/common.ts @@ -0,0 +1,43 @@ +import { ScaleBandSpec, ScaleBand, computeScale } from '@nivo/scales' + +/** + * Generates indexed scale. + */ +export const getIndexScale = ( + data: RawDatum[], + getIndex: (datum: RawDatum) => string, + padding: number, + indexScale: ScaleBandSpec, + size: number, + axis: 'x' | 'y' +) => { + return (computeScale( + indexScale, + { all: data.map(getIndex), min: 0, max: 0 }, + size, + axis + ) as ScaleBand).padding(padding) +} + +/** + * This method ensures all the provided keys exist in the entire series. + */ +export const normalizeData = (data: RawDatum[], keys: string[]) => + data.map( + item => + ({ + ...keys.reduce>((acc, key) => { + acc[key] = null + return acc + }, {}), + ...item, + } as RawDatum) + ) + +export const filterNullValues = >(data: RawDatum) => + Object.keys(data).reduce>((acc, key) => { + if (data[key]) { + acc[key] = data[key] + } + return acc + }, {}) as Exclude diff --git a/packages/bar/src/compute/grouped.js b/packages/bar/src/compute/grouped.js deleted file mode 100644 index 3747d7efa..000000000 --- a/packages/bar/src/compute/grouped.js +++ /dev/null @@ -1,212 +0,0 @@ -/* - * This file is part of the nivo project. - * - * Copyright 2016-present, Raphaël Benitte. - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -import { computeScale } from '@nivo/scales' -import { getIndexScale, filterNullValues, normalizeData } from './common' - -const gt = (value, other) => value > other -const lt = (value, other) => value < other - -const flatten = array => [].concat(...array) -const range = (start, end) => Array.from(' '.repeat(end - start), (_, index) => start + index) - -const clampToZero = value => (gt(value, 0) ? 0 : value) -const zeroIfNotFinite = value => (isFinite(value) ? value : 0) - -/** - * Generates x/y scales & bars for vertical grouped bar chart. - * - * @param {Array.} data - * @param {Function} getIndex - * @param {Array.} keys - * @param {number} minValue - * @param {number} maxValue - * @param {boolean} reverse - * @param {number} width - * @param {number} height - * @param {Function} getColor - * @param {number} [padding=0] - * @param {number} [innerPadding=0] - * @return {{ xScale: Function, yScale: Function, bars: Array. }} - */ -const generateVerticalGroupedBars = ( - { data, getIndex, keys, getColor, innerPadding, xScale, yScale }, - barWidth, - reverse, - yRef -) => { - const compare = reverse ? lt : gt - const getY = d => (compare(d, 0) ? yScale(d) : yRef) - const getHeight = (d, y) => (compare(d, 0) ? yRef - y : yScale(d) - yRef) - const cleanedData = data.map(filterNullValues) - - const bars = flatten( - keys.map((key, i) => - range(0, xScale.domain().length).map(index => { - const x = xScale(getIndex(data[index])) + barWidth * i + innerPadding * i - const y = getY(data[index][key]) - const barHeight = getHeight(data[index][key], y) - const barData = { - id: key, - value: data[index][key], - index, - indexValue: getIndex(data[index]), - data: cleanedData[index], - } - - return { - key: `${key}.${barData.indexValue}`, - data: barData, - x, - y, - width: barWidth, - height: barHeight, - color: getColor(barData), - } - }) - ) - ) - - return bars -} - -/** - * Generates x/y scales & bars for horizontal grouped bar chart. - * - * @param {Array.} data - * @param {Function} getIndex - * @param {Array.} keys - * @param {number} minValue - * @param {number} maxValue - * @param {boolean} reverse - * @param {number} width - * @param {number} height - * @param {Function} getColor - * @param {number} [padding=0] - * @param {number} [innerPadding=0] - * @return {{ xScale: Function, yScale: Function, bars: Array. }} - */ -const generateHorizontalGroupedBars = ( - { data, getIndex, keys, getColor, innerPadding = 0, xScale, yScale }, - barHeight, - reverse, - xRef -) => { - const compare = reverse ? lt : gt - const getX = d => (compare(d, 0) ? xRef : xScale(d)) - const getWidth = (d, x) => (compare(d, 0) ? xScale(d) - xRef : xRef - x) - const cleanedData = data.map(filterNullValues) - - const bars = flatten( - keys.map((key, i) => - range(0, yScale.domain().length).map(index => { - const x = getX(data[index][key]) - const y = yScale(getIndex(data[index])) + barHeight * i + innerPadding * i - const barWidth = getWidth(data[index][key], x) - const barData = { - id: key, - value: data[index][key], - index, - indexValue: getIndex(data[index]), - data: cleanedData[index], - } - - return { - key: `${key}.${barData.indexValue}`, - data: barData, - x, - y, - width: barWidth, - height: barHeight, - color: getColor(barData), - } - }) - ) - ) - - return bars -} - -/** - * Generates x/y scales & bars for grouped bar chart. - * - * @param {Object} options - * @return {{ xScale: Function, yScale: Function, bars: Array. }} - */ -export const generateGroupedBars = ({ - layout, - minValue, - maxValue, - reverse, - width, - height, - padding = 0, - innerPadding = 0, - valueScale, - indexScale: indexScaleConfig, - hiddenIds, - ...props -}) => { - const keys = props.keys.filter(key => !hiddenIds.includes(key)) - const data = normalizeData(props.data, keys) - const [axis, otherAxis, size] = layout === 'vertical' ? ['y', 'x', width] : ['x', 'y', height] - const indexScale = getIndexScale( - data, - props.getIndex, - padding, - indexScaleConfig, - size, - otherAxis - ) - - const scaleSpec = { - max: maxValue, - min: minValue, - reverse, - ...valueScale, - } - const clampMin = scaleSpec.min === 'auto' ? clampToZero : value => value - - const values = data - .reduce((acc, entry) => [...acc, ...keys.map(k => entry[k])], []) - .filter(Boolean) - const min = clampMin(Math.min(...values)) - const max = zeroIfNotFinite(Math.max(...values)) - - const scale = computeScale( - scaleSpec, - { all: values, min, max }, - axis === 'x' ? width : height, - axis - ) - - const [xScale, yScale] = layout === 'vertical' ? [indexScale, scale] : [scale, indexScale] - - const bandwidth = (indexScale.bandwidth() - innerPadding * (keys.length - 1)) / keys.length - const params = [ - { ...props, data, keys, innerPadding, xScale, yScale }, - bandwidth, - scaleSpec.reverse, - scale(0), - ] - - const bars = - bandwidth > 0 - ? layout === 'vertical' - ? generateVerticalGroupedBars(...params) - : generateHorizontalGroupedBars(...params) - : [] - - const legendData = props.keys.map(key => { - const bar = bars.find(bar => bar.data.id === key) || { data: {} } - - return { ...bar, data: { id: key, ...bar.data, hidden: hiddenIds.includes(key) } } - }) - - return { xScale, yScale, bars, legendData } -} diff --git a/packages/bar/src/compute/grouped.ts b/packages/bar/src/compute/grouped.ts new file mode 100644 index 000000000..84983b848 --- /dev/null +++ b/packages/bar/src/compute/grouped.ts @@ -0,0 +1,229 @@ +import { BarDatum, BarSvgProps, ComputedDatum } from '../types' +import { OrdinalColorScale } from '@nivo/colors' +import { Scale, ScaleBand } from '@nivo/scales' +import { computeScale } from '@nivo/scales' +import { getIndexScale, filterNullValues, normalizeData } from './common' + +type Params = { + data: RawDatum[] + getColor: OrdinalColorScale> + getIndex: (datum: RawDatum) => string + innerPadding: number + keys: string[] + xScale: XScaleInput extends string ? ScaleBand : Scale + yScale: YScaleInput extends string ? ScaleBand : Scale +} + +const gt = (value: number, other: number) => value > other +const lt = (value: number, other: number) => value < other + +const flatten = (array: T[][]) => ([] as T[]).concat(...array) +const range = (start: number, end: number) => + Array.from(' '.repeat(end - start), (_, index) => start + index) + +const clampToZero = (value: number) => (gt(value, 0) ? 0 : value) +const zeroIfNotFinite = (value: number) => (isFinite(value) ? value : 0) + +/** + * Generates x/y scales & bars for vertical grouped bar chart. + */ +const generateVerticalGroupedBars = >( + { + data, + getIndex, + keys, + getColor, + innerPadding = 0, + xScale, + yScale, + }: Params, + barWidth: number, + reverse: boolean, + yRef: number +) => { + const compare = reverse ? lt : gt + const getY = (d: number) => (compare(d, 0) ? yScale(d) ?? 0 : yRef) + const getHeight = (d: number, y: number) => (compare(d, 0) ? yRef - y : (yScale(d) ?? 0) - yRef) + const cleanedData = data.map(filterNullValues) + + const bars = flatten( + keys.map((key, i) => + range(0, xScale.domain().length).map(index => { + const value = Number(data[index][key]) + const indexValue = getIndex(data[index]) + const x = (xScale(indexValue) ?? 0) + barWidth * i + innerPadding * i + const y = getY(value) + const barHeight = getHeight(value, y) + const barData = { + id: key, + value, + index, + indexValue, + data: cleanedData[index], + } + + return { + key: `${key}.${barData.indexValue}`, + data: barData, + x, + y, + width: barWidth, + height: barHeight, + color: getColor(barData), + } + }) + ) + ) + + return bars +} + +/** + * Generates x/y scales & bars for horizontal grouped bar chart. + */ +const generateHorizontalGroupedBars = >( + { + data, + getIndex, + keys, + getColor, + innerPadding = 0, + xScale, + yScale, + }: Params, + barHeight: number, + reverse: boolean, + xRef: number +) => { + const compare = reverse ? lt : gt + const getX = (d: number) => (compare(d, 0) ? xRef : xScale(d) ?? 0) + const getWidth = (d: number, x: number) => (compare(d, 0) ? (xScale(d) ?? 0) - xRef : xRef - x) + const cleanedData = data.map(filterNullValues) + + const bars = flatten( + keys.map((key, i) => + range(0, yScale.domain().length).map(index => { + const value = Number(data[index][key]) + const indexValue = getIndex(data[index]) + const x = getX(value) + const y = (yScale(indexValue) ?? 0) + barHeight * i + innerPadding * i + const barWidth = getWidth(value, x) + const barData = { + id: key, + value, + index, + indexValue, + data: cleanedData[index], + } + + return { + key: `${key}.${barData.indexValue}`, + data: barData, + x, + y, + width: barWidth, + height: barHeight, + color: getColor(barData), + } + }) + ) + ) + + return bars +} + +/** + * Generates x/y scales & bars for grouped bar chart. + */ +export const generateGroupedBars = ({ + layout, + minValue, + maxValue, + reverse, + width, + height, + padding = 0, + innerPadding = 0, + valueScale, + indexScale: indexScaleConfig, + hiddenIds, + ...props +}: Pick< + Required>, + | 'data' + | 'height' + | 'indexScale' + | 'innerPadding' + | 'keys' + | 'layout' + | 'maxValue' + | 'minValue' + | 'padding' + | 'reverse' + | 'valueScale' + | 'width' +> & { + getColor: OrdinalColorScale> + getIndex: (datum: RawDatum) => string + hiddenIds: string[] +}) => { + const keys = props.keys.filter(key => !hiddenIds.includes(key)) + const data = normalizeData(props.data, keys) + const [axis, otherAxis, size] = + layout === 'vertical' ? (['y', 'x', width] as const) : (['x', 'y', height] as const) + const indexScale = getIndexScale( + data, + props.getIndex, + padding, + indexScaleConfig, + size, + otherAxis + ) + + const scaleSpec = { + max: maxValue, + min: minValue, + reverse, + ...valueScale, + } + + const clampMin = scaleSpec.min === 'auto' ? clampToZero : (value: number) => value + + const values = data + .reduce((acc, entry) => [...acc, ...keys.map(k => entry[k] as number)], []) + .filter(Boolean) + const min = clampMin(Math.min(...values)) + const max = zeroIfNotFinite(Math.max(...values)) + + const scale = computeScale( + scaleSpec as any, + { all: values, min, max }, + axis === 'x' ? width : height, + axis + ) + + const [xScale, yScale] = layout === 'vertical' ? [indexScale, scale] : [scale, indexScale] + + const bandwidth = (indexScale.bandwidth() - innerPadding * (keys.length - 1)) / keys.length + const params = [ + { ...props, data, keys, innerPadding, xScale, yScale } as Params, + bandwidth, + scaleSpec.reverse, + scale(0) ?? 0, + ] as const + + const bars = + bandwidth > 0 + ? layout === 'vertical' + ? generateVerticalGroupedBars(...params) + : generateHorizontalGroupedBars(...params) + : [] + + const legendData = props.keys.map(key => { + const bar = bars.find(bar => bar.data.id === key) || { data: {} } + + return { ...bar, data: { id: key, ...bar.data, hidden: hiddenIds.includes(key) } } + }) + + return { xScale, yScale, bars, legendData } +} diff --git a/packages/bar/src/compute/index.js b/packages/bar/src/compute/index.js deleted file mode 100644 index 998ee7283..000000000 --- a/packages/bar/src/compute/index.js +++ /dev/null @@ -1,11 +0,0 @@ -/* - * This file is part of the nivo project. - * - * Copyright 2016-present, Raphaël Benitte. - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -export * from './grouped' -export * from './stacked' -export * from './legends' diff --git a/packages/bar/src/compute/index.ts b/packages/bar/src/compute/index.ts new file mode 100644 index 000000000..a481adf51 --- /dev/null +++ b/packages/bar/src/compute/index.ts @@ -0,0 +1,3 @@ +export * from './grouped' +export * from './stacked' +export * from './legends' diff --git a/packages/bar/src/compute/legends.js b/packages/bar/src/compute/legends.js deleted file mode 100644 index 874809c7b..000000000 --- a/packages/bar/src/compute/legends.js +++ /dev/null @@ -1,55 +0,0 @@ -/* - * This file is part of the nivo project. - * - * Copyright 2016-present, Raphaël Benitte. - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -import { uniqBy } from 'lodash' - -export const getLegendDataForKeys = (bars, layout, direction, groupMode, reverse) => { - const data = uniqBy( - bars.map(bar => ({ - id: bar.data.id, - label: bar.data.label || bar.data.id, - hidden: bar.data.hidden, - color: bar.color, - fill: bar.data.fill, - })), - ({ id }) => id - ) - - if ( - (layout === 'vertical' && - groupMode === 'stacked' && - direction === 'column' && - reverse !== true) || - (layout === 'horizontal' && groupMode === 'stacked' && reverse === true) - ) { - data.reverse() - } - - return data -} - -export const getLegendDataForIndexes = bars => { - return uniqBy( - bars.map(bar => ({ - id: bar.data.indexValue, - label: bar.data.label || bar.data.indexValue, - hidden: bar.data.hidden, - color: bar.color, - fill: bar.data.fill, - })), - ({ id }) => id - ) -} - -export const getLegendData = ({ from, bars, layout, direction, groupMode, reverse }) => { - if (from === 'indexes') { - return getLegendDataForIndexes(bars) - } - - return getLegendDataForKeys(bars, layout, direction, groupMode, reverse) -} diff --git a/packages/bar/src/compute/legends.ts b/packages/bar/src/compute/legends.ts new file mode 100644 index 000000000..f89cf2122 --- /dev/null +++ b/packages/bar/src/compute/legends.ts @@ -0,0 +1,67 @@ +import { BarDatum, BarLegendProps, BarSvgProps, BarsWithHidden } from '../types' +import { uniqBy } from 'lodash' + +export const getLegendDataForKeys = ( + bars: BarsWithHidden, + layout: 'horizontal' | 'vertical', + direction: 'column' | 'row', + groupMode: 'grouped' | 'stacked', + reverse: boolean +) => { + const data = uniqBy( + bars.map(bar => ({ + id: bar.data.id, + // TODO: Add label accessor to make the following work: + // label: bar.data.label || bar.data.id, + label: bar.data.id, + hidden: bar.data.hidden, + color: bar.color, + })), + ({ id }) => id + ) + + if ( + (layout === 'vertical' && + groupMode === 'stacked' && + direction === 'column' && + reverse !== true) || + (layout === 'horizontal' && groupMode === 'stacked' && reverse === true) + ) { + data.reverse() + } + + return data +} + +export const getLegendDataForIndexes = (bars: BarsWithHidden) => { + return uniqBy( + bars.map(bar => ({ + id: bar.data.indexValue ?? '', + // TODO: Add label accessor to make the following work: + // label: bar.data.label || bar.data.indexValue, + label: bar.data.indexValue ?? '', + hidden: bar.data.hidden, + color: bar.color, + })), + ({ id }) => id + ) +} + +export const getLegendData = ({ + from, + bars, + layout, + direction, + groupMode, + reverse, +}: Pick>, 'layout' | 'groupMode' | 'reverse'> & { + bars: BarsWithHidden + direction: BarLegendProps['direction'] + from: BarLegendProps['dataFrom'] +}) => { + if (from === 'indexes') { + return getLegendDataForIndexes(bars) + } + + return getLegendDataForKeys(bars, layout, direction, groupMode, reverse) +} diff --git a/packages/bar/src/compute/stacked.js b/packages/bar/src/compute/stacked.ts similarity index 50% rename from packages/bar/src/compute/stacked.js rename to packages/bar/src/compute/stacked.ts index 63cbbe8b3..1abce539b 100644 --- a/packages/bar/src/compute/stacked.js +++ b/packages/bar/src/compute/stacked.ts @@ -1,59 +1,52 @@ -/* - * This file is part of the nivo project. - * - * Copyright 2016-present, Raphaël Benitte. - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -import { computeScale } from '@nivo/scales' -import { stack, stackOffsetDiverging } from 'd3-shape' +import { BarDatum, BarSvgProps, ComputedDatum } from '../types' +import { OrdinalColorScale } from '@nivo/colors' +import { Scale, ScaleBand, computeScale } from '@nivo/scales' +import { Series, SeriesPoint, stack, stackOffsetDiverging } from 'd3-shape' import { getIndexScale, filterNullValues, normalizeData } from './common' -const flattenDeep = (array, depth = 1) => - depth > 0 - ? array.reduce( - (acc, value) => - acc.concat(Array.isArray(value) ? flattenDeep(value, depth - 1) : value), - [] - ) - : array.slice() +type StackDatum = SeriesPoint + +type Params = { + getColor: OrdinalColorScale> + getIndex: (datum: RawDatum) => string + innerPadding: number + stackedData: Series[] + xScale: XScaleInput extends string ? ScaleBand : Scale + yScale: YScaleInput extends string ? ScaleBand : Scale +} + +const flattenDeep = (arr: T[]): T => + arr.some(Array.isArray) ? flattenDeep(([] as T[]).concat(...arr)) : ((arr as unknown) as T) /** * Generates x/y scales & bars for vertical stacked bar chart. - * - * @param {Array.} data - * @param {Function} getIndex - * @param {Array.} keys - * @param {number} minValue - * @param {number} maxValue - * @param {boolean} reverse - * @param {number} width - * @param {number} height - * @param {Function} getColor - * @param {number} [padding=0] - * @param {number} [innerPadding=0] - * @return {{ xScale: Function, yScale: Function, bars: Array. }} */ -const generateVerticalStackedBars = ( - { getIndex, getColor, innerPadding, stackedData, xScale, yScale }, - barWidth, - reverse +const generateVerticalStackedBars = >( + { + getIndex, + getColor, + innerPadding, + stackedData, + xScale, + yScale, + }: Params, + barWidth: number, + reverse: boolean ) => { - const getY = d => yScale(d[reverse ? 0 : 1]) - const getHeight = (d, y) => yScale(d[reverse ? 1 : 0]) - y + const getY = (d: StackDatum) => yScale(d[reverse ? 0 : 1]) + const getHeight = (d: StackDatum, y: number) => (yScale(d[reverse ? 1 : 0]) ?? 0) - y const bars = flattenDeep( stackedData.map(stackedDataItem => xScale.domain().map((index, i) => { const d = stackedDataItem[i] - const x = xScale(getIndex(d.data)) - const y = getY(d) + innerPadding * 0.5 + const x = xScale(getIndex(d.data)) ?? 0 + const y = (getY(d) ?? 0) + innerPadding * 0.5 const barHeight = getHeight(d, y) - innerPadding const barData = { id: stackedDataItem.key, - value: d.data[stackedDataItem.key], + value: Number(d.data[stackedDataItem.key]), index: i, indexValue: index, data: filterNullValues(d.data), @@ -77,39 +70,33 @@ const generateVerticalStackedBars = ( /** * Generates x/y scales & bars for horizontal stacked bar chart. - * - * @param {Array.} data - * @param {Function} getIndex - * @param {Array.} keys - * @param {number} minValue - * @param {number} maxValue - * @param {boolean} reverse - * @param {number} width - * @param {number} height - * @param {Function} getColor - * @param {number} [padding=0] - * @param {number} [innerPadding=0] - * @return {{ xScale: Function, yScale: Function, bars: Array. }} */ -const generateHorizontalStackedBars = ( - { getIndex, getColor, innerPadding, stackedData, xScale, yScale }, - barHeight, - reverse +const generateHorizontalStackedBars = >( + { + getIndex, + getColor, + innerPadding, + stackedData, + xScale, + yScale, + }: Params, + barHeight: number, + reverse: boolean ) => { - const getX = d => xScale(d[reverse ? 1 : 0]) - const getWidth = (d, x) => xScale(d[reverse ? 0 : 1]) - x + const getX = (d: StackDatum) => xScale(d[reverse ? 1 : 0]) + const getWidth = (d: StackDatum, x: number) => (xScale(d[reverse ? 0 : 1]) ?? 0) - x const bars = flattenDeep( stackedData.map(stackedDataItem => yScale.domain().map((index, i) => { const d = stackedDataItem[i] - const y = yScale(getIndex(d.data)) - const x = getX(d) + innerPadding * 0.5 + const y = yScale(getIndex(d.data)) ?? 0 + const x = (getX(d) ?? 0) + innerPadding * 0.5 const barWidth = getWidth(d, x) - innerPadding const barData = { id: stackedDataItem.key, - value: d.data[stackedDataItem.key], + value: Number(d.data[stackedDataItem.key]), index: i, indexValue: index, data: filterNullValues(d.data), @@ -133,11 +120,8 @@ const generateHorizontalStackedBars = ( /** * Generates x/y scales & bars for stacked bar chart. - * - * @param {Object} options - * @return {{ xScale: Function, yScale: Function, bars: Array. }} */ -export const generateStackedBars = ({ +export const generateStackedBars = ({ data, layout, minValue, @@ -150,11 +134,32 @@ export const generateStackedBars = ({ indexScale: indexScaleConfig, hiddenIds, ...props +}: Pick< + Required>, + | 'data' + | 'height' + | 'indexScale' + | 'innerPadding' + | 'keys' + | 'layout' + | 'maxValue' + | 'minValue' + | 'padding' + | 'reverse' + | 'valueScale' + | 'width' +> & { + getColor: OrdinalColorScale> + getIndex: (datum: RawDatum) => string + hiddenIds: string[] }) => { const keys = props.keys.filter(key => !hiddenIds.includes(key)) - const stackedData = stack().keys(keys).offset(stackOffsetDiverging)(normalizeData(data, keys)) + const stackedData = stack().keys(keys).offset(stackOffsetDiverging)( + normalizeData(data, keys) + ) - const [axis, otherAxis, size] = layout === 'vertical' ? ['y', 'x', width] : ['x', 'y', height] + const [axis, otherAxis, size] = + layout === 'vertical' ? (['y', 'x', width] as const) : (['x', 'y', height] as const) const indexScale = getIndexScale( data, props.getIndex, @@ -171,12 +176,12 @@ export const generateStackedBars = ({ ...valueScale, } - const values = flattenDeep(stackedData, 2) + const values = flattenDeep((stackedData as unknown) as number[][]) const min = Math.min(...values) const max = Math.max(...values) const scale = computeScale( - scaleSpec, + scaleSpec as any, { all: values, min, max }, axis === 'x' ? width : height, axis @@ -187,10 +192,10 @@ export const generateStackedBars = ({ const innerPadding = props.innerPadding > 0 ? props.innerPadding : 0 const bandwidth = indexScale.bandwidth() const params = [ - { ...props, innerPadding, stackedData, xScale, yScale }, + { ...props, innerPadding, stackedData, xScale, yScale } as Params, bandwidth, scaleSpec.reverse, - ] + ] as const const bars = bandwidth > 0 @@ -200,14 +205,9 @@ export const generateStackedBars = ({ : [] const legendData = props.keys.map(key => { - const bar = bars.find(bar => bar.data.id === key) || { - data: {}, - } - - return { - ...bar, - data: { id: key, ...bar.data, hidden: hiddenIds.includes(key) }, - } + const bar = bars.find(bar => bar.data.id === key) || { data: {} } + + return { ...bar, data: { id: key, ...bar.data, hidden: hiddenIds.includes(key) } } }) return { xScale, yScale, bars, legendData } diff --git a/packages/bar/src/enhance.js b/packages/bar/src/enhance.js deleted file mode 100644 index 3fb1e9b6f..000000000 --- a/packages/bar/src/enhance.js +++ /dev/null @@ -1,53 +0,0 @@ -/* - * This file is part of the nivo project. - * - * Copyright 2016-present, Raphaël Benitte. - *d - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -import { compose, defaultProps, pure, withPropsOnChange } from '@nivo/recompose' -import { - withTheme, - withDimensions, - withMotion, - getPropertyAccessor, - getLabelGenerator, -} from '@nivo/core' -import { getOrdinalColorScale, getInheritedColorGenerator } from '@nivo/colors' -import { BarDefaultProps } from './props' - -export default Component => - compose( - defaultProps(BarDefaultProps), - withTheme(), - withDimensions(), - withMotion(), - withPropsOnChange(['colors', 'colorBy'], ({ colors, colorBy }) => ({ - getColor: getOrdinalColorScale(colors, colorBy), - })), - withPropsOnChange(['indexBy'], ({ indexBy }) => ({ - getIndex: getPropertyAccessor(indexBy), - })), - withPropsOnChange(['labelTextColor', 'theme'], ({ labelTextColor, theme }) => ({ - getLabelTextColor: getInheritedColorGenerator(labelTextColor, theme), - })), - withPropsOnChange(['labelLinkColor', 'theme'], ({ labelLinkColor, theme }) => ({ - getLabelLinkColor: getInheritedColorGenerator(labelLinkColor, theme), - })), - withPropsOnChange(['label', 'labelFormat'], ({ label, labelFormat }) => ({ - getLabel: getLabelGenerator(label, labelFormat), - })), - withPropsOnChange(['borderColor', 'theme'], ({ borderColor, theme }) => ({ - getBorderColor: getInheritedColorGenerator(borderColor, theme), - })), - withPropsOnChange(['tooltipLabel'], ({ tooltipLabel }) => { - let getTooltipLabel = d => `${d.id} - ${d.indexValue}` - if (typeof tooltipLabel === 'function') { - getTooltipLabel = tooltipLabel - } - - return { getTooltipLabel } - }), - pure - )(Component) diff --git a/packages/bar/src/index.js b/packages/bar/src/index.js deleted file mode 100644 index fc764529a..000000000 --- a/packages/bar/src/index.js +++ /dev/null @@ -1,14 +0,0 @@ -/* - * This file is part of the nivo project. - * - * Copyright 2016-present, Raphaël Benitte. - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -export { default as Bar } from './Bar' -export { default as BarItem } from './BarItem' -export { default as BarCanvas } from './BarCanvas' -export { default as ResponsiveBar } from './ResponsiveBar' -export { default as ResponsiveBarCanvas } from './ResponsiveBarCanvas' -export * from './props' diff --git a/packages/bar/src/index.ts b/packages/bar/src/index.ts new file mode 100644 index 000000000..f0548c4d5 --- /dev/null +++ b/packages/bar/src/index.ts @@ -0,0 +1,7 @@ +export * from './Bar' +export * from './BarItem' +export * from './BarCanvas' +export * from './ResponsiveBar' +export * from './ResponsiveBarCanvas' +export * from './props' +export * from './types' diff --git a/packages/bar/src/props.js b/packages/bar/src/props.js deleted file mode 100644 index fae7095db..000000000 --- a/packages/bar/src/props.js +++ /dev/null @@ -1,162 +0,0 @@ -/* - * This file is part of the nivo project. - * - * Copyright 2016-present, Raphaël Benitte. - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -import PropTypes from 'prop-types' -import { noop, defsPropTypes } from '@nivo/core' -import { - ordinalColorsPropType, - colorPropertyAccessorPropType, - inheritedColorPropType, -} from '@nivo/colors' -import { axisPropType } from '@nivo/axes' -import { LegendPropShape } from '@nivo/legends' -import BarItem from './BarItem' -import BarTooltip from './BarTooltip' - -export const BarPropTypes = { - data: PropTypes.arrayOf(PropTypes.object).isRequired, - indexBy: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired, - getIndex: PropTypes.func.isRequired, // computed - keys: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])).isRequired, - layers: PropTypes.arrayOf( - PropTypes.oneOfType([ - PropTypes.oneOf(['grid', 'axes', 'bars', 'markers', 'legends', 'annotations']), - PropTypes.func, - ]) - ).isRequired, - - groupMode: PropTypes.oneOf(['stacked', 'grouped']).isRequired, - layout: PropTypes.oneOf(['horizontal', 'vertical']).isRequired, - reverse: PropTypes.bool.isRequired, - valueScale: PropTypes.object.isRequired, - indexScale: PropTypes.object.isRequired, - - minValue: PropTypes.oneOfType([PropTypes.number, PropTypes.oneOf(['auto'])]).isRequired, - maxValue: PropTypes.oneOfType([PropTypes.number, PropTypes.oneOf(['auto'])]).isRequired, - padding: PropTypes.number.isRequired, - innerPadding: PropTypes.number.isRequired, - - axisTop: axisPropType, - axisRight: axisPropType, - axisBottom: axisPropType, - axisLeft: axisPropType, - enableGridX: PropTypes.bool.isRequired, - enableGridY: PropTypes.bool.isRequired, - gridXValues: PropTypes.oneOfType([ - PropTypes.number, - PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.number, PropTypes.string])), - ]), - gridYValues: PropTypes.oneOfType([ - PropTypes.number, - PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.number, PropTypes.string])), - ]), - - barComponent: PropTypes.func.isRequired, - - enableLabel: PropTypes.bool.isRequired, - label: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired, - labelFormat: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), - getLabel: PropTypes.func.isRequired, // computed - labelSkipWidth: PropTypes.number.isRequired, - labelSkipHeight: PropTypes.number.isRequired, - labelTextColor: inheritedColorPropType.isRequired, - getLabelTextColor: PropTypes.func.isRequired, // computed - labelLinkColor: inheritedColorPropType.isRequired, - getLabelLinkColor: PropTypes.func.isRequired, // computed - - colors: ordinalColorsPropType.isRequired, - colorBy: colorPropertyAccessorPropType.isRequired, - borderRadius: PropTypes.number.isRequired, - getColor: PropTypes.func.isRequired, // computed - ...defsPropTypes, - borderWidth: PropTypes.number.isRequired, - borderColor: inheritedColorPropType.isRequired, - getBorderColor: PropTypes.func.isRequired, - - isInteractive: PropTypes.bool, - onClick: PropTypes.func.isRequired, - onMouseEnter: PropTypes.func.isRequired, - onMouseLeave: PropTypes.func.isRequired, - tooltipLabel: PropTypes.func, - getTooltipLabel: PropTypes.func.isRequired, - tooltipFormat: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), - tooltip: PropTypes.func, - - legends: PropTypes.arrayOf( - PropTypes.shape({ - dataFrom: PropTypes.oneOf(['indexes', 'keys']).isRequired, - ...LegendPropShape, - }) - ).isRequired, - - renderWrapper: PropTypes.bool, - pixelRatio: PropTypes.number.isRequired, -} - -export const BarSvgPropTypes = { - ...BarPropTypes, - role: PropTypes.string.isRequired, -} - -export const BarDefaultProps = { - indexBy: 'id', - keys: ['value'], - layers: ['grid', 'axes', 'bars', 'markers', 'legends', 'annotations'], - - groupMode: 'stacked', - layout: 'vertical', - reverse: false, - - minValue: 'auto', - maxValue: 'auto', - - valueScale: { type: 'linear' }, - indexScale: { type: 'band', round: true }, - - padding: 0.1, - innerPadding: 0, - - axisBottom: {}, - axisLeft: {}, - enableGridX: false, - enableGridY: true, - - barComponent: BarItem, - - enableLabel: true, - label: 'value', - labelSkipWidth: 0, - labelSkipHeight: 0, - labelLinkColor: 'theme', - labelTextColor: 'theme', - - colors: { scheme: 'nivo' }, - colorBy: 'id', - defs: [], - fill: [], - borderRadius: 0, - borderWidth: 0, - borderColor: { from: 'color' }, - - isInteractive: true, - tooltip: BarTooltip, - onClick: noop, - onMouseEnter: noop, - onMouseLeave: noop, - - legends: [], - - annotations: [], - - pixelRatio: typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1, -} - -export const BarSvgDefaultProps = { - ...BarDefaultProps, - role: 'img', -} diff --git a/packages/bar/src/props.ts b/packages/bar/src/props.ts new file mode 100644 index 000000000..a606f4f80 --- /dev/null +++ b/packages/bar/src/props.ts @@ -0,0 +1,68 @@ +import { BarTooltip } from './BarTooltip' +import { ComputedDatum } from './types' +import { ScaleSpec, ScaleBandSpec } from '@nivo/scales' +import { InheritedColorConfig, OrdinalColorScaleConfig } from '@nivo/colors' +import { BarItem } from './BarItem' + +export const defaultProps = { + indexBy: 'id', + keys: ['value'], + layers: ['grid', 'axes', 'bars', 'markers', 'legends', 'annotations'], + + groupMode: 'stacked' as const, + layout: 'vertical' as const, + reverse: false, + + minValue: 'auto' as const, + maxValue: 'auto' as const, + + valueScale: { type: 'linear' } as ScaleSpec, + indexScale: { type: 'band', round: true } as ScaleBandSpec, + + padding: 0.1, + innerPadding: 0, + + axisBottom: {}, + axisLeft: {}, + enableGridX: false, + enableGridY: true, + + barComponent: BarItem, + + enableLabel: true, + label: 'value', + labelSkipWidth: 0, + labelSkipHeight: 0, + labelLinkColor: 'theme', + labelTextColor: 'theme', + + colorBy: 'id' as const, + colors: { scheme: 'nivo' } as OrdinalColorScaleConfig, + defs: [], + fill: [], + borderRadius: 0, + borderWidth: 0, + borderColor: { from: 'color' } as InheritedColorConfig, + + isInteractive: true, + tooltip: BarTooltip, + tooltipLabel: (datum: ComputedDatum) => `${datum.id} - ${datum.indexValue}`, + + animate: true, + motionStiffness: 90, + motionDamping: 15, + + legends: [], + + annotations: [], +} + +export const svgDefaultProps = { + ...defaultProps, + role: 'img', +} + +export const canvasDefaultProps = { + ...defaultProps, + pixelRatio: typeof window !== 'undefined' ? window.devicePixelRatio ?? 1 : 1, +} diff --git a/packages/bar/src/types.ts b/packages/bar/src/types.ts new file mode 100644 index 000000000..d87f125bf --- /dev/null +++ b/packages/bar/src/types.ts @@ -0,0 +1,223 @@ +import * as React from 'react' +import { AnnotationMatcher } from '@nivo/annotations' +import { AxisProps, GridValues } from '@nivo/axes' +import { + Box, + CartesianMarkerProps, + Dimensions, + MotionProps, + PropertyAccessor, + SvgDefsAndFill, + Theme, + ValueFormat, +} from '@nivo/core' +import { InheritedColorConfig, OrdinalColorScaleConfig } from '@nivo/colors' +import { LegendProps } from '@nivo/legends' +import { Scale, ScaleSpec, ScaleBandSpec } from '@nivo/scales' + +export interface BarDatum { + [key: string]: string | number +} + +export interface DataProps { + data: RawDatum[] +} + +export type BarDatumWithColor = BarDatum & { + color: string +} + +export type ComputedDatum = { + id: string | number + value: number + index: number + indexValue: string | number + data: Exclude + fill?: string +} + +export type ComputedBarDatum = { + key: string + data: ComputedDatum + x: number + y: number + width: number + height: number + color: string +} + +export type BarsWithHidden = Array< + Partial<{ + key: string + x: number + y: number + width: number + height: number + color: string + }> & { + data: Partial> & { + id: string | number + hidden: boolean + } + } +> + +export type LegendData = { + id: string | number + label: string | number + hidden: boolean + color: string +} + +export interface BarLegendProps extends LegendProps { + dataFrom: 'indexes' | 'keys' +} + +export type LabelFormatter = (label: string | number) => string | number +export type ValueFormatter = (value: number) => string | number + +export type BarLayerId = 'grid' | 'axes' | 'bars' | 'markers' | 'legends' | 'annotations' + +export interface BarCustomLayerProps + extends Pick< + BarCommonProps, + | 'borderRadius' + | 'borderWidth' + | 'enableLabel' + | 'labelSkipHeight' + | 'labelSkipWidth' + | 'tooltip' + | 'tooltipFormat' + >, + TooltipHandlers, + BarHandlers { + bars: ComputedBarDatum[] + legendData: BarsWithHidden + + getTooltipLabel: (datum: ComputedDatum) => string | number + + xScale: Scale + yScale: Scale +} + +export type BarCustomLayer = React.FC> + +export type BarLayer = BarLayerId | BarCustomLayer + +export interface BarItemProps + extends Pick< + BarCommonProps, + 'borderRadius' | 'borderWidth' | 'tooltip' | 'tooltipFormat' + >, + ComputedBarDatum, + TooltipHandlers, + BarHandlers { + borderColor: string + + label: string + labelColor: string + shouldRenderLabel: boolean + + getTooltipLabel: (datum: ComputedDatum) => string | number +} + +export interface BarTooltipProps extends ComputedDatum { + color: string + getTooltipLabel: (datum: ComputedDatum) => string | number + tooltipFormat: BarCommonProps['tooltipFormat'] +} + +export type BarHandlers = { + onClick?: ( + datum: ComputedDatum & { color: string }, + event: React.MouseEvent + ) => void + onMouseEnter?: (datum: ComputedDatum, event: React.MouseEvent) => void + onMouseLeave?: (datum: ComputedDatum, event: React.MouseEvent) => void +} + +export type BarCommonProps = { + indexBy: PropertyAccessor + keys: string[] + + maxValue: 'auto' | number + minValue: 'auto' | number + + margin?: Box + innerPadding: number + padding: number + + barComponent: React.FC> + + valueScale: ScaleSpec + indexScale: ScaleBandSpec + + axisBottom?: AxisProps + axisLeft?: AxisProps + axisRight?: AxisProps + axisTop?: AxisProps + + enableGridX: boolean + gridXValues?: GridValues + enableGridY: boolean + gridYValues?: GridValues + + borderColor: InheritedColorConfig> + borderRadius: number + borderWidth: number + + enableLabel: boolean + label: PropertyAccessor, string> + labelFormat: string | LabelFormatter + labelSkipWidth: number + labelSkipHeight: number + labelTextColor: InheritedColorConfig> + + isInteractive: boolean + + tooltip: React.FC> + tooltipLabel: PropertyAccessor, string> + tooltipFormat?: ValueFormat + + groupMode: 'grouped' | 'stacked' + layout: 'horizontal' | 'vertical' + reverse: boolean + + colorBy: 'id' | 'indexValue' + colors: OrdinalColorScaleConfig> + theme: Theme + + annotations: AnnotationMatcher>[] + legends: BarLegendProps[] + markers?: CartesianMarkerProps[] + + renderWrapper?: boolean +} + +export type BarSvgProps = Partial> & + DataProps & + BarHandlers & + SvgDefsAndFill> & + Dimensions & + MotionProps & + Partial<{ + layers: BarLayer[] + role: string + }> + +export type BarCanvasProps = Partial> & + DataProps & + BarHandlers & + Dimensions & { + pixelRatio?: number + } + +export type BarAnnotationsProps = { + annotations: AnnotationMatcher>[] + bars: ComputedBarDatum[] +} + +export type TooltipHandlers = { + hideTooltip: () => void + showTooltip: (content: JSX.Element, event: React.MouseEvent) => void +} diff --git a/packages/bar/tsconfig.json b/packages/bar/tsconfig.json new file mode 100644 index 000000000..855b4b2b7 --- /dev/null +++ b/packages/bar/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.types.json", + "compilerOptions": { + "outDir": "./dist/types", + "rootDir": "./src" + }, + "include": ["src/**/*"] +} diff --git a/tsconfig.monorepo.json b/tsconfig.monorepo.json index 65c56e55d..1d0b2fdb3 100644 --- a/tsconfig.monorepo.json +++ b/tsconfig.monorepo.json @@ -18,13 +18,14 @@ { "path": "./packages/recompose" }, // Charts now - { "path": "./packages/voronoi" }, + { "path": "./packages/bar" }, { "path": "./packages/bullet" }, { "path": "./packages/calendar" }, { "path": "./packages/circle-packing" }, { "path": "./packages/marimekko" }, { "path": "./packages/pie" }, { "path": "./packages/sunburst" }, - { "path": "./packages/swarmplot" } + { "path": "./packages/swarmplot" }, + { "path": "./packages/voronoi" } ] }