From d90a3261c053ac2e4018985c9879dddabcbc4ea2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Matias?= Date: Mon, 18 Mar 2024 22:59:13 -0300 Subject: [PATCH] feat(bar): add bar totals (#2525) * feat: add bar totals layer * fix: insert unique key prop to each total * test: insert tests to totals bar layer * docs: insert recipe on website bar page * fix: update scale functions types * refactor: enable totals by prop instead of directly on layers * style: apply theme configuration on totals * feat: make totals offsets configurable * refactor: centralize compute of totals bar * chore: re-format website docs yml * refactor: use props along with layers to enable totals * fix: remove unnused variable * style: add transitions to bar totals * test: update tests to find totals component * docs: add enable totals docs on website * refactor: use totals computed value through hook on canvas * fix: remove unnused var * fix: add enableTotals prop to default bar props on website * docs(website): add default enableTotals prop to canvas and svg flavors * style: align total label text based on layout mode * feat: add value format to totals labels * refactor: configure totals transition inside its component * refactor: format value in totals compute function * style: prevent overlap on zero totals offset * types: remove optional syntax * feat: animation offset is calculated individually by index * chore: change order of initializing default layers * refactor: add numeric value to bar totals data --- packages/bar/src/Bar.tsx | 42 ++++-- packages/bar/src/BarCanvas.tsx | 32 +++++ packages/bar/src/BarTotals.tsx | 75 +++++++++++ packages/bar/src/compute/totals.ts | 155 ++++++++++++++++++++++ packages/bar/src/hooks.ts | 9 ++ packages/bar/src/index.ts | 3 +- packages/bar/src/props.ts | 9 +- packages/bar/src/types.ts | 10 +- packages/bar/tests/Bar.test.tsx | 161 ++++++++++++++++++++++- storybook/stories/bar/Bar.stories.tsx | 4 + website/src/data/components/bar/meta.yml | 10 +- website/src/data/components/bar/props.ts | 25 ++++ website/src/pages/bar/api.tsx | 2 + website/src/pages/bar/canvas.js | 2 + website/src/pages/bar/index.js | 2 + 15 files changed, 512 insertions(+), 29 deletions(-) create mode 100644 packages/bar/src/BarTotals.tsx create mode 100644 packages/bar/src/compute/totals.ts diff --git a/packages/bar/src/Bar.tsx b/packages/bar/src/Bar.tsx index 11ec1d2a1..d0dd26ec1 100644 --- a/packages/bar/src/Bar.tsx +++ b/packages/bar/src/Bar.tsx @@ -1,14 +1,4 @@ import { Axes, Grid } from '@nivo/axes' -import { BarAnnotations } from './BarAnnotations' -import { - BarCustomLayerProps, - BarDatum, - BarLayer, - BarLayerId, - BarSvgProps, - ComputedBarDatumWithValue, -} from './types' -import { BarLegends } from './BarLegends' import { CartesianMarkers, Container, @@ -18,10 +8,21 @@ import { useDimensions, useMotionConfig, } from '@nivo/core' -import { Fragment, ReactNode, createElement, useMemo } from 'react' -import { svgDefaultProps } from './props' import { useTransition } from '@react-spring/web' +import { Fragment, ReactNode, createElement, useMemo } from 'react' +import { BarAnnotations } from './BarAnnotations' +import { BarLegends } from './BarLegends' import { useBar } from './hooks' +import { svgDefaultProps } from './props' +import { + BarCustomLayerProps, + BarDatum, + BarLayer, + BarLayerId, + BarSvgProps, + ComputedBarDatumWithValue, +} from './types' +import { BarTotals } from './BarTotals' type InnerBarProps = Omit< BarSvgProps, @@ -102,6 +103,9 @@ const InnerBar = ({ barAriaDescribedBy, initialHiddenIds, + + enableTotals = svgDefaultProps.enableTotals, + totalsOffset = svgDefaultProps.totalsOffset, }: InnerBarProps) => { const { animate, config: springConfig } = useMotionConfig() const { outerWidth, outerHeight, margin, innerWidth, innerHeight } = useDimensions( @@ -122,6 +126,7 @@ const InnerBar = ({ shouldRenderBarLabel, toggleSerie, legendsWithData, + barTotals, } = useBar({ indexBy, label, @@ -151,6 +156,7 @@ const InnerBar = ({ legends, legendLabel, initialHiddenIds, + totalsOffset, }) const transition = useTransition< @@ -283,6 +289,7 @@ const InnerBar = ({ grid: null, legends: null, markers: null, + totals: null, } if (layers.includes('annotations')) { @@ -362,6 +369,17 @@ const InnerBar = ({ ) } + if (layers.includes('totals') && enableTotals) { + layerById.totals = ( + + ) + } + const layerContext: BarCustomLayerProps = useMemo( () => ({ ...commonProps, diff --git a/packages/bar/src/BarCanvas.tsx b/packages/bar/src/BarCanvas.tsx index 7f69196a6..fefd2a04f 100644 --- a/packages/bar/src/BarCanvas.tsx +++ b/packages/bar/src/BarCanvas.tsx @@ -2,16 +2,19 @@ import { BarCanvasCustomLayerProps, BarCanvasLayer, BarCanvasProps, + BarCommonProps, BarDatum, ComputedBarDatum, } from './types' import { + CompleteTheme, Container, Margin, getRelativeCursor, isCursorInRect, useDimensions, useTheme, + useValueFormatter, } from '@nivo/core' import { ForwardedRef, @@ -32,6 +35,7 @@ import { renderAxesToCanvas, renderGridLinesToCanvas } from '@nivo/axes' import { renderLegendToCanvas } from '@nivo/legends' import { useTooltip } from '@nivo/tooltip' import { useBar } from './hooks' +import { BarTotalsData } from './compute/totals' type InnerBarCanvasProps = Omit< BarCanvasProps, @@ -52,6 +56,22 @@ const findBarUnderCursor = ( const isNumber = (value: unknown): value is number => typeof value === 'number' +function renderTotalsToCanvas( + ctx: CanvasRenderingContext2D, + barTotals: BarTotalsData[], + theme: CompleteTheme, + layout: BarCommonProps['layout'] = canvasDefaultProps.layout +) { + ctx.fillStyle = theme.text.fill + ctx.font = `bold ${theme.labels.text.fontSize}px ${theme.labels.text.fontFamily}` + ctx.textBaseline = layout === 'vertical' ? 'alphabetic' : 'middle' + ctx.textAlign = layout === 'vertical' ? 'center' : 'start' + + barTotals.forEach(barTotal => { + ctx.fillText(barTotal.formattedValue, barTotal.x, barTotal.y) + }) +} + const InnerBarCanvas = ({ data, indexBy, @@ -166,6 +186,9 @@ const InnerBarCanvas = ({ pixelRatio = canvasDefaultProps.pixelRatio, canvasRef, + + enableTotals = canvasDefaultProps.enableTotals, + totalsOffset = canvasDefaultProps.totalsOffset, }: InnerBarCanvasProps) => { const canvasEl = useRef(null) @@ -187,6 +210,7 @@ const InnerBarCanvas = ({ getLabelColor, shouldRenderBarLabel, legendsWithData, + barTotals, } = useBar({ indexBy, label, @@ -215,6 +239,7 @@ const InnerBarCanvas = ({ labelSkipHeight, legends, legendLabel, + totalsOffset, }) const { showTooltipFromEvent, hideTooltip } = useTooltip() @@ -285,6 +310,8 @@ const InnerBarCanvas = ({ ] ) + const formatValue = useValueFormatter(valueFormat) + useEffect(() => { const ctx = canvasEl.current?.getContext('2d') @@ -362,6 +389,8 @@ const InnerBarCanvas = ({ }) } else if (layer === 'annotations') { renderAnnotationsToCanvas(ctx, { annotations: boundAnnotations, theme }) + } else if (layer === 'totals' && enableTotals) { + renderTotalsToCanvas(ctx, barTotals, theme, layout) } else if (typeof layer === 'function') { layer(ctx, layerContext) } @@ -404,6 +433,9 @@ const InnerBarCanvas = ({ shouldRenderBarLabel, theme, width, + barTotals, + enableTotals, + formatValue, ]) const handleMouseHover = useCallback( diff --git a/packages/bar/src/BarTotals.tsx b/packages/bar/src/BarTotals.tsx new file mode 100644 index 000000000..dc6c0ea5c --- /dev/null +++ b/packages/bar/src/BarTotals.tsx @@ -0,0 +1,75 @@ +import { useTheme } from '@nivo/core' +import { AnimationConfig, animated, useTransition } from '@react-spring/web' +import { BarCommonProps, BarDatum } from './types' +import { svgDefaultProps } from './props' +import { BarTotalsData } from './compute/totals' + +interface Props { + data: BarTotalsData[] + springConfig: Partial + animate: boolean + layout?: BarCommonProps['layout'] +} + +export const BarTotals = ({ + data, + springConfig, + animate, + layout = svgDefaultProps.layout, +}: Props) => { + const theme = useTheme() + const totalsTransition = useTransition< + BarTotalsData, + { + x: number + y: number + labelOpacity: number + } + >(data, { + keys: barTotal => barTotal.key, + from: barTotal => ({ + x: layout === 'vertical' ? barTotal.x : barTotal.animationOffset, + y: layout === 'vertical' ? barTotal.animationOffset : barTotal.y, + labelOpacity: 0, + }), + enter: barTotal => ({ + x: barTotal.x, + y: barTotal.y, + labelOpacity: 1, + }), + update: barTotal => ({ + x: barTotal.x, + y: barTotal.y, + labelOpacity: 1, + }), + leave: barTotal => ({ + x: layout === 'vertical' ? barTotal.x : barTotal.animationOffset, + y: layout === 'vertical' ? barTotal.animationOffset : barTotal.y, + labelOpacity: 0, + }), + config: springConfig, + immediate: !animate, + initial: animate ? undefined : null, + }) + + return totalsTransition((style, barTotal) => ( + + {barTotal.formattedValue} + + )) +} diff --git a/packages/bar/src/compute/totals.ts b/packages/bar/src/compute/totals.ts new file mode 100644 index 000000000..f9b29ceaa --- /dev/null +++ b/packages/bar/src/compute/totals.ts @@ -0,0 +1,155 @@ +import { AnyScale, ScaleBand } from '@nivo/scales' +import { defaultProps } from '../props' +import { BarCommonProps, BarDatum, ComputedBarDatum } from '../types' + +export interface BarTotalsData { + key: string + x: number + y: number + value: number + formattedValue: string + animationOffset: number +} + +export const computeBarTotals = ( + bars: ComputedBarDatum[], + xScale: ScaleBand | AnyScale, + yScale: ScaleBand | AnyScale, + layout: BarCommonProps['layout'] = defaultProps.layout, + groupMode: BarCommonProps['groupMode'] = defaultProps.groupMode, + totalsOffset: number, + formatValue: (value: number) => string +) => { + const totals = [] as BarTotalsData[] + + if (bars.length === 0) return totals + + const totalsByIndex = new Map() + + const barWidth = bars[0].width + const barHeight = bars[0].height + + if (groupMode === 'stacked') { + const totalsPositivesByIndex = new Map() + + bars.forEach(bar => { + const { indexValue, value } = bar.data + updateTotalsByIndex(totalsByIndex, indexValue, Number(value)) + updateTotalsPositivesByIndex(totalsPositivesByIndex, indexValue, Number(value)) + }) + + totalsPositivesByIndex.forEach((totalsPositive, indexValue) => { + const indexTotal = totalsByIndex.get(indexValue) || 0 + + let xPosition: number + let yPosition: number + let animationOffset: number + + if (layout === 'vertical') { + xPosition = xScale(indexValue) + yPosition = yScale(totalsPositive) + animationOffset = yScale(totalsPositive / 2) + } else { + xPosition = xScale(totalsPositive) + yPosition = yScale(indexValue) + animationOffset = xScale(totalsPositive / 2) + } + + xPosition += layout === 'vertical' ? barWidth / 2 : totalsOffset + yPosition += layout === 'vertical' ? -totalsOffset : barHeight / 2 + + totals.push({ + key: 'total_' + indexValue, + x: xPosition, + y: yPosition, + value: indexTotal, + formattedValue: formatValue(indexTotal), + animationOffset, + }) + }) + } else if (groupMode === 'grouped') { + const greatestValueByIndex = new Map() + const numberOfBarsByIndex = new Map() + + bars.forEach(bar => { + const { indexValue, value } = bar.data + updateTotalsByIndex(totalsByIndex, indexValue, Number(value)) + updateGreatestValueByIndex(greatestValueByIndex, indexValue, Number(value)) + updateNumberOfBarsByIndex(numberOfBarsByIndex, indexValue) + }) + + greatestValueByIndex.forEach((greatestValue, indexValue) => { + const indexTotal = totalsByIndex.get(indexValue) || 0 + const numberOfBars = numberOfBarsByIndex.get(indexValue) + + let xPosition: number + let yPosition: number + let animationOffset: number + + if (layout === 'vertical') { + xPosition = xScale(indexValue) + yPosition = yScale(greatestValue) + animationOffset = yScale(greatestValue / 2) + } else { + xPosition = xScale(greatestValue) + yPosition = yScale(indexValue) + animationOffset = xScale(greatestValue / 2) + } + + const indexBarsWidth = numberOfBars * barWidth + const indexBarsHeight = numberOfBars * barHeight + + xPosition += layout === 'vertical' ? indexBarsWidth / 2 : totalsOffset + yPosition += layout === 'vertical' ? -totalsOffset : indexBarsHeight / 2 + + totals.push({ + key: 'total_' + indexValue, + x: xPosition, + y: yPosition, + value: indexTotal, + formattedValue: formatValue(indexTotal), + animationOffset, + }) + }) + } + return totals +} + +// this function is used to compute the total value for the indexes. The total value is later rendered on the chart +export const updateTotalsByIndex = ( + totalsByIndex: Map, + indexValue: string | number, + value: number +) => { + const currentIndexValue = totalsByIndex.get(indexValue) || 0 + totalsByIndex.set(indexValue, currentIndexValue + value) +} + +// this function is used to compute only the positive values of the indexes. Useful to position the text right above the last stacked bar. It prevents overlapping in case of negative values +export const updateTotalsPositivesByIndex = ( + totalsPositivesByIndex: Map, + indexValue: string | number, + value: number +) => { + const currentIndexValue = totalsPositivesByIndex.get(indexValue) || 0 + totalsPositivesByIndex.set(indexValue, currentIndexValue + (value > 0 ? value : 0)) +} + +// this function is used to keep track of the highest value for the indexes. Useful to position the text above the longest grouped bar +export const updateGreatestValueByIndex = ( + greatestValueByIndex: Map, + indexValue: string | number, + value: number +) => { + const currentGreatestValue = greatestValueByIndex.get(indexValue) || 0 + greatestValueByIndex.set(indexValue, Math.max(currentGreatestValue, Number(value))) +} + +// this function is used to save the number of bars for the indexes. Useful to position the text in the middle of the grouped bars +export const updateNumberOfBarsByIndex = ( + numberOfBarsByIndex: Map, + indexValue: string | number +) => { + const currentNumberOfBars = numberOfBarsByIndex.get(indexValue) || 0 + numberOfBarsByIndex.set(indexValue, currentNumberOfBars + 1) +} diff --git a/packages/bar/src/hooks.ts b/packages/bar/src/hooks.ts index 880b5983b..635b711c4 100644 --- a/packages/bar/src/hooks.ts +++ b/packages/bar/src/hooks.ts @@ -11,6 +11,7 @@ import { } from './types' import { defaultProps } from './props' import { generateGroupedBars, generateStackedBars, getLegendData } from './compute' +import { computeBarTotals } from './compute/totals' export const useBar = ({ indexBy = defaultProps.indexBy, @@ -41,6 +42,7 @@ export const useBar = ({ labelSkipHeight = defaultProps.labelSkipHeight, legends = defaultProps.legends, legendLabel, + totalsOffset = defaultProps.totalsOffset, }: { indexBy?: BarCommonProps['indexBy'] label?: BarCommonProps['label'] @@ -70,6 +72,7 @@ export const useBar = ({ labelSkipHeight?: BarCommonProps['labelSkipHeight'] legends?: BarCommonProps['legends'] legendLabel?: BarCommonProps['legendLabel'] + totalsOffset?: BarCommonProps['totalsOffset'] }) => { const [hiddenIds, setHiddenIds] = useState(initialHiddenIds ?? []) const toggleSerie = useCallback((id: string | number) => { @@ -167,6 +170,11 @@ export const useBar = ({ [legends, legendData, bars, groupMode, layout, legendLabel, reverse] ) + const barTotals = useMemo( + () => computeBarTotals(bars, xScale, yScale, layout, groupMode, totalsOffset, formatValue), + [bars, xScale, yScale, layout, groupMode, totalsOffset, formatValue] + ) + return { bars, barsWithValue, @@ -183,5 +191,6 @@ export const useBar = ({ hiddenIds, toggleSerie, legendsWithData, + barTotals, } } diff --git a/packages/bar/src/index.ts b/packages/bar/src/index.ts index 179dd8da6..45855e52b 100644 --- a/packages/bar/src/index.ts +++ b/packages/bar/src/index.ts @@ -1,7 +1,8 @@ export * from './Bar' +export * from './BarCanvas' export * from './BarItem' export * from './BarTooltip' -export * from './BarCanvas' +export * from './BarTotals' export * from './ResponsiveBar' export * from './ResponsiveBarCanvas' export * from './props' diff --git a/packages/bar/src/props.ts b/packages/bar/src/props.ts index e069933c7..98c53fcf7 100644 --- a/packages/bar/src/props.ts +++ b/packages/bar/src/props.ts @@ -1,6 +1,6 @@ import { BarItem } from './BarItem' import { BarTooltip } from './BarTooltip' -import { ComputedDatum } from './types' +import { BarCanvasLayerId, BarLayerId, ComputedDatum } from './types' import { InheritedColorConfig, OrdinalColorScaleConfig } from '@nivo/colors' import { ScaleBandSpec, ScaleSpec } from '@nivo/scales' @@ -47,11 +47,14 @@ export const defaultProps = { initialHiddenIds: [], annotations: [], markers: [], + + enableTotals: false, + totalsOffset: 10, } export const svgDefaultProps = { ...defaultProps, - layers: ['grid', 'axes', 'bars', 'markers', 'legends', 'annotations'], + layers: ['grid', 'axes', 'bars', 'totals', 'markers', 'legends', 'annotations'] as BarLayerId[], barComponent: BarItem, defs: [], @@ -66,7 +69,7 @@ export const svgDefaultProps = { export const canvasDefaultProps = { ...defaultProps, - layers: ['grid', 'axes', 'bars', 'legends', 'annotations'], + layers: ['grid', 'axes', 'bars', 'totals', 'legends', 'annotations'] as BarCanvasLayerId[], pixelRatio: typeof window !== 'undefined' ? window.devicePixelRatio ?? 1 : 1, } diff --git a/packages/bar/src/types.ts b/packages/bar/src/types.ts index 24e8ea52e..ec545851e 100644 --- a/packages/bar/src/types.ts +++ b/packages/bar/src/types.ts @@ -95,7 +95,8 @@ export interface BarLegendProps extends LegendProps { 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 type BarLayerId = 'grid' | 'axes' | 'bars' | 'markers' | 'legends' | 'annotations' | 'totals' +export type BarCanvasLayerId = Exclude interface BarCustomLayerBaseProps extends Pick< @@ -138,9 +139,7 @@ export type BarCanvasCustomLayer = ( ) => void export type BarCustomLayer = React.FC> -export type BarCanvasLayer = - | Exclude - | BarCanvasCustomLayer +export type BarCanvasLayer = BarCanvasLayerId | BarCanvasCustomLayer export type BarLayer = BarLayerId | BarCustomLayer export interface BarItemProps @@ -259,6 +258,9 @@ export type BarCommonProps = { renderWrapper?: boolean initialHiddenIds: readonly (string | number)[] + + enableTotals: boolean + totalsOffset: number } export type BarSvgProps = Partial> & diff --git a/packages/bar/tests/Bar.test.tsx b/packages/bar/tests/Bar.test.tsx index 37cbc1ae5..1fe68574c 100644 --- a/packages/bar/tests/Bar.test.tsx +++ b/packages/bar/tests/Bar.test.tsx @@ -1,7 +1,7 @@ import { mount } from 'enzyme' -import { create, act, ReactTestRenderer } from 'react-test-renderer' +import { create, act, ReactTestRenderer, type ReactTestInstance } from 'react-test-renderer' import { LegendSvg, LegendSvgItem } from '@nivo/legends' -import { Bar, BarDatum, BarItemProps, ComputedDatum, BarItem, BarTooltip } from '../' +import { Bar, BarDatum, BarItemProps, ComputedDatum, BarItem, BarTooltip, BarTotals } from '../' type IdValue = { id: string @@ -614,6 +614,163 @@ it('should render bars in grouped mode after updating starting values from 0', ( }) }) +describe('totals layer', () => { + it('should have the total text for each index with vertical layout', () => { + const instance = create( + + ).root + + const totals = instance.findByType(BarTotals).findAllByType('text') + + totals.forEach((total, index) => { + const value = total.findByType('text').children[0] + if (index === 0) { + expect(value).toBe(`2`) + } else if (index === 1) { + expect(value).toBe(`3`) + } else if (index === 2) { + expect(value).toBe(`4`) + } + }) + }) + it('should have the total text for each index with horizontal layout', () => { + const instance = create( + + ).root + + const totals = instance.findByType(BarTotals).findAllByType('text') + + totals.forEach((total, index) => { + const value = total.findByType('text').children[0] + if (index === 0) { + expect(value).toBe(`$2`) + } else if (index === 1) { + expect(value).toBe(`$4`) + } else if (index === 2) { + expect(value).toBe(`$6`) + } + }) + }) + it('should have the total text for each index with grouped group mode and vertical layout', () => { + const instance = create( + + ).root + + const totals = instance.findByType(BarTotals).findAllByType('text') + + totals.forEach((total, index) => { + const value = total.findByType('text').children[0] + if (index === 0) { + expect(value).toBe(`-2`) + } else { + expect(value).toBe(`-4`) + } + }) + }) + it('should have the total text for each index with grouped group mode and horizontal layout', () => { + const instance = create( + + ).root + + const totals = instance.findByType(BarTotals).findAllByType('text') + + totals.forEach((total, index) => { + const value = total.findByType('text').children[0] + if (index === 0) { + expect(value).toBe(`0`) + } else if (index === 1) { + expect(value).toBe(`1`) + } else if (index === 2) { + expect(value).toBe(`3`) + } + }) + }) + it('should follow the theme configurations', () => { + const instance = create( + + ).root + + const totals = instance.findByType(BarTotals).findAllByType('text') + + totals.forEach((total, index) => { + const props = total.findByType('text').props + expect(props.style.fill).toBe('red') + expect(props.fontSize).toBe(14) + expect(props.fontFamily).toBe('serif') + }) + }) +}) + describe('tooltip', () => { it('should render a tooltip when hovering a slice', () => { let component: ReactTestRenderer diff --git a/storybook/stories/bar/Bar.stories.tsx b/storybook/stories/bar/Bar.stories.tsx index aa2a3b832..2c9ddb5f6 100644 --- a/storybook/stories/bar/Bar.stories.tsx +++ b/storybook/stories/bar/Bar.stories.tsx @@ -294,6 +294,10 @@ export const WithSymlogScale: Story = { ), } +export const WithTotals: Story = { + render: () => , +} + const DataGenerator = (initialIndex, initialState) => { let index = initialIndex let state = initialState diff --git a/website/src/data/components/bar/meta.yml b/website/src/data/components/bar/meta.yml index 6af1929ba..989f3706c 100644 --- a/website/src/data/components/bar/meta.yml +++ b/website/src/data/components/bar/meta.yml @@ -34,21 +34,19 @@ Bar: link: bar--custom-legend-labels - label: Using annotations link: bar--with-annotations + - label: Using totals + link: bar--with-totals description: | Bar chart which can display multiple data series, stacked or side by side. Also supports both vertical and horizontal layout, with negative values descending below the x axis (or y axis if using horizontal layout). - The bar item component can be customized to render any valid SVG element, it will receive current bar style, data and event handlers, the storybook offers an [example](storybook:bar--custom-bar-item). - The responsive alternative of this component is `ResponsiveBar`. - This component is available in the `@nivo/api`, see [sample](api:/samples/bar.svg) or [try it using the API client](self:/bar/api). - See the [dedicated guide](self:/guides/legends) on how to setup legends for this component. However it requires an extra property for each legend configuration you pass to @@ -56,7 +54,6 @@ Bar: legend's data and accept `indexes` or `keys`. `indexes` is suitable for simple bar chart with a single data serie while `keys` may be used if you have several ones (groups). - BarCanvas: package: '@nivo/bar' tags: @@ -72,5 +69,4 @@ BarCanvas: A variation around the [Bar](self:/bar) component. Well suited for large data sets as it does not impact DOM tree depth, however you'll lose the isomorphic ability and transitions. - - The responsive alternative of this component is `ResponsiveBarCanvas`. + The responsive alternative of this component is `ResponsiveBarCanvas`. \ No newline at end of file diff --git a/website/src/data/components/bar/props.ts b/website/src/data/components/bar/props.ts index ba5111e69..2e5cd406b 100644 --- a/website/src/data/components/bar/props.ts +++ b/website/src/data/components/bar/props.ts @@ -416,6 +416,31 @@ const props: ChartProperty[] = [ control: { type: 'inheritedColor' }, group: 'Labels', }, + { + key: 'enableTotals', + help: 'Enable/disable totals labels.', + type: 'boolean', + flavors: ['svg', 'canvas', 'api'], + required: false, + defaultValue: svgDefaultProps.enableTotals, + group: 'Labels', + control: { type: 'switch' }, + }, + { + key: 'totalsOffset', + help: 'Offset from the bar edge for the total label.', + type: 'number', + flavors: ['svg', 'canvas', 'api'], + required: false, + defaultValue: svgDefaultProps.totalsOffset, + group: 'Labels', + control: { + type: 'range', + unit: 'px', + min: 0, + max: 40, + }, + }, ...chartGrid({ flavors: allFlavors, xDefault: svgDefaultProps.enableGridX, diff --git a/website/src/pages/bar/api.tsx b/website/src/pages/bar/api.tsx index 5c30e2571..198fe6191 100644 --- a/website/src/pages/bar/api.tsx +++ b/website/src/pages/bar/api.tsx @@ -109,6 +109,8 @@ const BarApi = () => { enableGridY: true, enableLabel: true, + enableTotals: false, + totalsOffset: 10, labelSkipWidth: 12, labelSkipHeight: 12, labelTextColor: { diff --git a/website/src/pages/bar/canvas.js b/website/src/pages/bar/canvas.js index 398521c0d..f053bd1d5 100644 --- a/website/src/pages/bar/canvas.js +++ b/website/src/pages/bar/canvas.js @@ -89,6 +89,8 @@ const initialProperties = { enableGridY: false, enableLabel: true, + enableTotals: false, + totalsOffset: 10, labelSkipWidth: 12, labelSkipHeight: 12, labelTextColor: { diff --git a/website/src/pages/bar/index.js b/website/src/pages/bar/index.js index 74254db8e..e7c91f2d1 100644 --- a/website/src/pages/bar/index.js +++ b/website/src/pages/bar/index.js @@ -107,6 +107,8 @@ const initialProperties = { enableGridY: true, enableLabel: true, + enableTotals: false, + totalsOffset: 10, labelSkipWidth: 12, labelSkipHeight: 12, labelTextColor: {