diff --git a/packages/bar/src/Bar.tsx b/packages/bar/src/Bar.tsx index 9d3dbd390..b925e946d 100644 --- a/packages/bar/src/Bar.tsx +++ b/packages/bar/src/Bar.tsx @@ -332,9 +332,14 @@ const InnerBar = ({ const layerContext: any = useMemo( () => ({ ...commonProps, + margin, + innerWidth, + innerHeight, + width, + height, ...result, }), - [commonProps, result] + [commonProps, height, innerHeight, innerWidth, margin, result, width] ) return ( diff --git a/packages/bar/src/BarCanvas.tsx b/packages/bar/src/BarCanvas.tsx index 291a869ca..ea97581d9 100644 --- a/packages/bar/src/BarCanvas.tsx +++ b/packages/bar/src/BarCanvas.tsx @@ -1,4 +1,4 @@ -import { BarCanvasProps, BarDatum, ComputedBarDatum } from './types' +import { BarCanvasLayer, BarCanvasProps, BarDatum, ComputedBarDatum } from './types' import { Container, Margin, @@ -17,7 +17,6 @@ import { useEffect, useMemo, useRef, - useState, } from 'react' import { canvasDefaultProps } from './props' import { generateGroupedBars, generateStackedBars, getLegendData } from './compute' @@ -55,6 +54,8 @@ const findBarUnderCursor = ( isCursorInRect(node.x + margin.left, node.y + margin.top, node.width, node.height, x, y) ) +const isNumber = (value: unknown): value is number => typeof value === 'number' + const InnerBarCanvas = ({ data, indexBy = canvasDefaultProps.indexBy, @@ -85,6 +86,9 @@ const InnerBarCanvas = ({ gridXValues, gridYValues, + layers = canvasDefaultProps.layers as BarCanvasLayer[], + // barComponent = svgDefaultProps.barComponent, + enableLabel = canvasDefaultProps.enableLabel, label = canvasDefaultProps.label, labelSkipWidth = canvasDefaultProps.labelSkipWidth, @@ -118,13 +122,6 @@ const InnerBarCanvas = ({ }: InnerBarCanvasProps) => { const canvasEl = useRef(null) - const [hiddenIds] = useState([]) - // const toggleSerie = useCallback(id => { - // setHiddenIds(state => - // state.indexOf(id) > -1 ? state.filter(item => item !== id) : [...state, id] - // ) - // }, []) - const theme = useTheme() const { margin, innerWidth, innerHeight, outerWidth, outerHeight } = useDimensions( width, @@ -157,7 +154,7 @@ const InnerBarCanvas = ({ innerPadding, valueScale, indexScale, - hiddenIds, + hiddenIds: [], formatValue, } @@ -169,9 +166,9 @@ const InnerBarCanvas = ({ keys.map(key => { const bar = result.bars.find(bar => bar.data.id === key) - return { ...bar, data: { id: key, ...bar?.data, hidden: hiddenIds.includes(key) } } + return { ...bar, data: { id: key, ...bar?.data, hidden: false } } }), - [hiddenIds, keys, result.bars] + [keys, result.bars] ) const shouldRenderLabel = useCallback( @@ -201,6 +198,48 @@ const InnerBarCanvas = ({ }), }) + // We use `any` here until we can figure out the best way to type xScale/yScale + const layerContext: any = useMemo( + () => ({ + borderRadius, + borderWidth, + enableLabel, + isInteractive, + labelSkipWidth, + labelSkipHeight, + onClick, + onMouseEnter, + onMouseLeave, + getTooltipLabel, + tooltip, + margin, + innerWidth, + innerHeight, + width, + height, + ...result, + }), + [ + borderRadius, + borderWidth, + enableLabel, + getTooltipLabel, + height, + innerHeight, + innerWidth, + isInteractive, + labelSkipHeight, + labelSkipWidth, + margin, + onClick, + onMouseEnter, + onMouseLeave, + result, + tooltip, + width, + ] + ) + useEffect(() => { const ctx = canvasEl.current?.getContext('2d') @@ -216,112 +255,114 @@ const InnerBarCanvas = ({ ctx.fillRect(0, 0, outerWidth, outerHeight) ctx.translate(margin.left, margin.top) - if (theme.grid.line?.strokeWidth !== undefined) { - ctx.lineWidth = theme.grid.line.strokeWidth as any - ctx.strokeStyle = theme.grid.line.stroke as any - - if (enableGridX) { - renderGridLinesToCanvas(ctx, { - width, - height, - scale: result.xScale as any, - axis: 'x', - values: gridXValues, + layers.forEach(layer => { + if (layer === 'grid') { + if (isNumber(theme.grid.line.strokeWidth) && theme.grid.line.strokeWidth > 0) { + ctx.lineWidth = theme.grid.line.strokeWidth + ctx.strokeStyle = theme.grid.line.stroke as string + + if (enableGridX) { + renderGridLinesToCanvas(ctx, { + width, + height, + scale: result.xScale as any, + axis: 'x', + values: gridXValues, + }) + } + + if (enableGridY) { + renderGridLinesToCanvas(ctx, { + width, + height, + scale: result.yScale as any, + axis: 'y', + values: gridYValues, + }) + } + } + } else if (layer === 'axes') { + renderAxesToCanvas(ctx, { + xScale: result.xScale as any, + yScale: result.yScale as any, + width: innerWidth, + height: innerHeight, + top: axisTop, + right: axisRight, + bottom: axisBottom, + left: axisLeft, + theme, }) - } - - if (enableGridY) { - renderGridLinesToCanvas(ctx, { - width, - height, - scale: result.yScale as any, - axis: 'y', - values: gridYValues, + } else if (layer === 'bars') { + result.bars.forEach(bar => { + const { x, y, color, width, height } = bar + + ctx.fillStyle = color + + if (borderWidth > 0) { + ctx.strokeStyle = getBorderColor(bar) + ctx.lineWidth = borderWidth + } + + ctx.beginPath() + + if (borderRadius > 0) { + const radius = Math.min(borderRadius, height) + + ctx.moveTo(x + radius, y) + ctx.lineTo(x + width - radius, y) + ctx.quadraticCurveTo(x + width, y, x + width, y + radius) + ctx.lineTo(x + width, y + height - radius) + ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height) + ctx.lineTo(x + radius, y + height) + ctx.quadraticCurveTo(x, y + height, x, y + height - radius) + ctx.lineTo(x, y + radius) + ctx.quadraticCurveTo(x, y, x + radius, y) + ctx.closePath() + } else { + ctx.rect(x, y, width, height) + } + + ctx.fill() + + if (borderWidth > 0) { + ctx.stroke() + } + + if (shouldRenderLabel({ height, width })) { + ctx.textBaseline = 'middle' + ctx.textAlign = 'center' + ctx.fillStyle = getLabelColor(bar) + ctx.fillText(getLabel(bar.data), x + width / 2, y + height / 2) + } }) - } - } - - ctx.save() - - ctx.strokeStyle = '#dddddd' - - legends.forEach(legend => { - const data = getLegendData({ - bars: legendData, - direction: legend.direction, - from: legend.dataFrom, - groupMode, - layout, - legendLabel, - reverse, - }) - - renderLegendToCanvas(ctx, { - ...legend, - data, - containerWidth: innerWidth, - containerHeight: innerHeight, - theme, - }) - }) - - renderAxesToCanvas(ctx, { - xScale: result.xScale as any, - yScale: result.yScale as any, - width: innerWidth, - height: innerHeight, - top: axisTop, - right: axisRight, - bottom: axisBottom, - left: axisLeft, - theme, - }) - - result.bars.forEach(bar => { - const { x, y, color, width, height } = bar - - ctx.fillStyle = color - - if (borderWidth > 0) { - ctx.strokeStyle = getBorderColor(bar) - ctx.lineWidth = borderWidth - } - - ctx.beginPath() - - if (borderRadius > 0) { - const radius = Math.min(borderRadius, height) - - ctx.moveTo(x + radius, y) - ctx.lineTo(x + width - radius, y) - ctx.quadraticCurveTo(x + width, y, x + width, y + radius) - ctx.lineTo(x + width, y + height - radius) - ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height) - ctx.lineTo(x + radius, y + height) - ctx.quadraticCurveTo(x, y + height, x, y + height - radius) - ctx.lineTo(x, y + radius) - ctx.quadraticCurveTo(x, y, x + radius, y) - ctx.closePath() - } else { - ctx.rect(x, y, width, height) - } - - ctx.fill() - - if (borderWidth > 0) { - ctx.stroke() - } - - if (shouldRenderLabel({ height, width })) { - ctx.textBaseline = 'middle' - ctx.textAlign = 'center' - ctx.fillStyle = getLabelColor(bar) - ctx.fillText(getLabel(bar.data), x + width / 2, y + height / 2) + } else if (layer === 'legends') { + legends.forEach(legend => { + const data = getLegendData({ + bars: legendData, + direction: legend.direction, + from: legend.dataFrom, + groupMode, + layout, + legendLabel, + reverse, + }) + + renderLegendToCanvas(ctx, { + ...legend, + data, + containerWidth: innerWidth, + containerHeight: innerHeight, + theme, + }) + }) + } else if (layer === 'annotations') { + renderAnnotationsToCanvas(ctx, { annotations: boundAnnotations, theme }) + } else if (typeof layer === 'function') { + layer(ctx, layerContext) } }) - renderAnnotationsToCanvas(ctx, { annotations: boundAnnotations, theme }) - ctx.save() }, [ axisBottom, @@ -342,6 +383,8 @@ const InnerBarCanvas = ({ height, innerHeight, innerWidth, + layerContext, + layers, layout, legendData, legendLabel, diff --git a/packages/bar/src/props.ts b/packages/bar/src/props.ts index 8be106fc5..4f435f3fa 100644 --- a/packages/bar/src/props.ts +++ b/packages/bar/src/props.ts @@ -7,7 +7,6 @@ import { ScaleBandSpec, ScaleSpec } from '@nivo/scales' export const defaultProps = { indexBy: 'id', keys: ['value'], - layers: ['grid', 'axes', 'bars', 'markers', 'legends', 'annotations'], groupMode: 'stacked' as const, layout: 'vertical' as const, @@ -27,8 +26,6 @@ export const defaultProps = { enableGridX: false, enableGridY: true, - barComponent: BarItem, - enableLabel: true, label: 'formattedValue', labelSkipWidth: 0, @@ -38,15 +35,14 @@ export const defaultProps = { 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}`, + tooltipLabel: (datum: ComputedDatum) => `${datum.id} - ${datum.indexValue}`, legends: [], @@ -55,12 +51,21 @@ export const defaultProps = { export const svgDefaultProps = { ...defaultProps, + layers: ['grid', 'axes', 'bars', 'markers', 'legends', 'annotations'], + barComponent: BarItem, + + defs: [], + fill: [], + animate: true, motionConfig: 'gentle', + role: 'img', } export const canvasDefaultProps = { ...defaultProps, + layers: ['grid', 'axes', 'bars', 'legends', 'annotations'], + pixelRatio: typeof window !== 'undefined' ? window.devicePixelRatio ?? 1 : 1, } diff --git a/packages/bar/src/types.ts b/packages/bar/src/types.ts index a16addfb5..489336cc3 100644 --- a/packages/bar/src/types.ts +++ b/packages/bar/src/types.ts @@ -5,6 +5,7 @@ import { Box, CartesianMarkerProps, Dimensions, + Margin, ModernMotionProps, PropertyAccessor, SvgDefsAndFill, @@ -102,18 +103,30 @@ export interface BarCustomLayerProps | 'labelSkipWidth' | 'tooltip' >, + Dimensions, BarHandlers { bars: ComputedBarDatum[] legendData: BarsWithHidden + margin: Margin + innerWidth: number + innerHeight: number + getTooltipLabel: (datum: ComputedDatum) => string | number xScale: Scale yScale: Scale } +export type BarCanvasCustomLayer = ( + context: CanvasRenderingContext2D, + props: BarCustomLayerProps +) => void export type BarCustomLayer = React.FC> +export type BarCanvasLayer = + | Exclude + | BarCanvasCustomLayer export type BarLayer = BarLayerId | BarCustomLayer export interface BarItemProps @@ -242,6 +255,7 @@ export type BarCanvasProps = Partial axisTop: CanvasAxisProp + layers: BarCanvasLayer[] pixelRatio: number }> diff --git a/packages/bar/stories/barCanvas.stories.tsx b/packages/bar/stories/barCanvas.stories.tsx index b1d843d78..14c4348dc 100644 --- a/packages/bar/stories/barCanvas.stories.tsx +++ b/packages/bar/stories/barCanvas.stories.tsx @@ -1,7 +1,8 @@ import { useRef } from 'react' import { storiesOf } from '@storybook/react' +import { withKnobs, boolean } from '@storybook/addon-knobs' import { generateCountriesData } from '@nivo/generators' -import { BarCanvas, BarDatum } from '../src' +import { BarCanvas, BarCanvasLayer, BarDatum, canvasDefaultProps } from '../src' import { button } from '@storybook/addon-knobs' const keys = ['hot dogs', 'burgers', 'sandwich', 'kebab', 'fries', 'donut'] @@ -19,6 +20,50 @@ const commonProps = { const stories = storiesOf('BarCanvas', module) +stories.addDecorator(withKnobs) + +stories.add('default', () => ) + +stories.add('custom layer', () => { + const layers = canvasDefaultProps.layers.filter(layer => + boolean(`layer.${layer}`, true) + ) as BarCanvasLayer[] + + return ( + { + const total = props.bars + .reduce((acc, bar) => acc + bar.data.value, 0) + .toLocaleString() + + ctx.save() + + ctx.textAlign = 'right' + ctx.font = 'bold 20px san-serif' + ctx.fillStyle = '#2a2a2a' + + ctx.fillText(`Grand Total: ${total}`, props.width - 100, -10) + + ctx.restore() + }, + ]} + legends={[ + { + anchor: 'bottom', + dataFrom: 'keys', + direction: 'row', + itemHeight: 20, + itemWidth: 110, + translateY: 50, + }, + ]} + /> + ) +}) + stories.add('custom tooltip', () => (