From 2c7d970a7ed83fa86a5896adf2c0a0f1d0461c9e Mon Sep 17 00:00:00 2001 From: joaopedromatias Date: Sat, 2 Mar 2024 17:57:38 -0300 Subject: [PATCH 01/28] feat: add bar totals layer --- packages/bar/src/Bar.tsx | 15 +++ packages/bar/src/BarCanvas.tsx | 99 ++++++++++++++++- packages/bar/src/BarTotals.tsx | 151 ++++++++++++++++++++++++++ packages/bar/src/index.ts | 1 + packages/bar/src/props.ts | 6 +- packages/bar/src/types.ts | 7 +- storybook/stories/bar/Bar.stories.tsx | 9 ++ 7 files changed, 280 insertions(+), 8 deletions(-) create mode 100644 packages/bar/src/BarTotals.tsx diff --git a/packages/bar/src/Bar.tsx b/packages/bar/src/Bar.tsx index 11ec1d2a1c..e648801466 100644 --- a/packages/bar/src/Bar.tsx +++ b/packages/bar/src/Bar.tsx @@ -22,6 +22,7 @@ import { Fragment, ReactNode, createElement, useMemo } from 'react' import { svgDefaultProps } from './props' import { useTransition } from '@react-spring/web' import { useBar } from './hooks' +import { BarTotals } from './BarTotals' type InnerBarProps = Omit< BarSvgProps, @@ -283,6 +284,7 @@ const InnerBar = ({ grid: null, legends: null, markers: null, + totals: null, } if (layers.includes('annotations')) { @@ -362,6 +364,19 @@ const InnerBar = ({ ) } + if (layers.includes('totals')) { + layerById.totals = ( + + ) + } + const layerContext: BarCustomLayerProps = useMemo( () => ({ ...commonProps, diff --git a/packages/bar/src/BarCanvas.tsx b/packages/bar/src/BarCanvas.tsx index 7f69196a6c..8f44138239 100644 --- a/packages/bar/src/BarCanvas.tsx +++ b/packages/bar/src/BarCanvas.tsx @@ -2,6 +2,7 @@ import { BarCanvasCustomLayerProps, BarCanvasLayer, BarCanvasProps, + BarCommonProps, BarDatum, ComputedBarDatum, } from './types' @@ -22,7 +23,7 @@ import { useMemo, useRef, } from 'react' -import { canvasDefaultProps } from './props' +import { canvasDefaultProps, defaultProps } from './props' import { renderAnnotationsToCanvas, useAnnotations, @@ -32,6 +33,12 @@ import { renderAxesToCanvas, renderGridLinesToCanvas } from '@nivo/axes' import { renderLegendToCanvas } from '@nivo/legends' import { useTooltip } from '@nivo/tooltip' import { useBar } from './hooks' +import { + updateGreatestValueByIndex, + updateNumberOfBarsByIndex, + updateTotalsByIndex, + updateTotalsPositivesByIndex, +} from './BarTotals' type InnerBarCanvasProps = Omit< BarCanvasProps, @@ -52,6 +59,93 @@ const findBarUnderCursor = ( const isNumber = (value: unknown): value is number => typeof value === 'number' +function renderTotalsToCanvas( + ctx: CanvasRenderingContext2D, + bars: ComputedBarDatum[], + xScale: (value: string | number) => number, + yScale: (value: string | number) => number, + layout: BarCommonProps['layout'] = defaultProps.layout, + groupMode: BarCommonProps['groupMode'] = defaultProps.groupMode +) { + if (bars.length === 0) return + + const totalsByIndex = new Map() + + const barWidth = bars[0].width + const barHeight = bars[0].height + const yOffsetVertically = -10 + const xOffsetHorizontally = 20 + const fontSize = 12 + + ctx.fillStyle = '#222222' + ctx.font = `${fontSize}px sans-serif` + ctx.textBaseline = 'middle' + ctx.textAlign = 'center' + + 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) => { + let xPosition: number + let yPosition: number + + if (layout === 'vertical') { + xPosition = xScale(indexValue) + yPosition = yScale(totalsPositive) + } else { + xPosition = xScale(totalsPositive) + yPosition = yScale(indexValue) + } + + ctx.fillText( + String(totalsByIndex.get(indexValue)), + xPosition + (layout === 'vertical' ? barWidth / 2 : xOffsetHorizontally), + yPosition + (layout === 'vertical' ? yOffsetVertically : barHeight / 2) + ) + }) + } + + 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) => { + let xPosition: number + let yPosition: number + + if (layout === 'vertical') { + xPosition = xScale(indexValue) + yPosition = yScale(greatestValue) + } else { + xPosition = xScale(greatestValue) + yPosition = yScale(indexValue) + } + + const indexBarsWidth = numberOfBarsByIndex.get(indexValue) * barWidth + const indexBarsHeight = numberOfBarsByIndex.get(indexValue) * barHeight + + ctx.fillText( + String(totalsByIndex.get(indexValue)), + xPosition + (layout === 'vertical' ? indexBarsWidth / 2 : xOffsetHorizontally), + yPosition + (layout === 'vertical' ? yOffsetVertically : indexBarsHeight / 2) + ) + }) + } +} + const InnerBarCanvas = ({ data, indexBy, @@ -362,6 +456,8 @@ const InnerBarCanvas = ({ }) } else if (layer === 'annotations') { renderAnnotationsToCanvas(ctx, { annotations: boundAnnotations, theme }) + } else if (layer === 'totals') { + renderTotalsToCanvas(ctx, bars, xScale, yScale, layout, groupMode) } else if (typeof layer === 'function') { layer(ctx, layerContext) } @@ -404,6 +500,7 @@ const InnerBarCanvas = ({ shouldRenderBarLabel, theme, width, + bars, ]) const handleMouseHover = useCallback( diff --git a/packages/bar/src/BarTotals.tsx b/packages/bar/src/BarTotals.tsx new file mode 100644 index 0000000000..e3f95d918a --- /dev/null +++ b/packages/bar/src/BarTotals.tsx @@ -0,0 +1,151 @@ +import { defaultProps } from './props' +import { BarCommonProps, BarDatum, ComputedBarDatum } from './types' + +interface BarTotalsProps { + bars: ComputedBarDatum[] + xScale: (value: string | number) => number + yScale: (value: string | number) => number + layout?: BarCommonProps['layout'] + groupMode?: BarCommonProps['groupMode'] +} + +export const BarTotals = ({ + bars, + xScale, + yScale, + layout = defaultProps.layout, + groupMode = defaultProps.groupMode, +}: BarTotalsProps) => { + if (bars.length === 0) return <> + const totals: JSX.Element[] = [] + + const totalsByIndex = new Map() + + const barWidth = bars[0].width + const barHeight = bars[0].height + const yOffsetVertically = -10 + const xOffsetHorizontally = 20 + const fontSize = 12 + + const commonProps = { + fill: '#222222', + fontWeight: 'bold', + fontSize, + textAnchor: 'middle', + alignmentBaseline: 'middle', + } as const + + 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) => { + let xPosition: number + let yPosition: number + + if (layout === 'vertical') { + xPosition = xScale(indexValue) + yPosition = yScale(totalsPositive) + } else { + xPosition = xScale(totalsPositive) + yPosition = yScale(indexValue) + } + + totals.push( + + {totalsByIndex.get(indexValue)} + + ) + }) + } + + 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) => { + let xPosition: number + let yPosition: number + + if (layout === 'vertical') { + xPosition = xScale(indexValue) + yPosition = yScale(greatestValue) + } else { + xPosition = xScale(greatestValue) + yPosition = yScale(indexValue) + } + + const indexBarsWidth = numberOfBarsByIndex.get(indexValue) * barWidth + const indexBarsHeight = numberOfBarsByIndex.get(indexValue) * barHeight + + totals.push( + + {totalsByIndex.get(indexValue)} + + ) + }) + } + + return <>{totals} +} + +export const updateTotalsByIndex = ( + totalsByIndex: Map, + indexValue: string | number, + value: number +) => { + const currentIndexValue = totalsByIndex.get(indexValue) || 0 + totalsByIndex.set(indexValue, currentIndexValue + value) +} + +export const updateTotalsPositivesByIndex = ( + totalsPositivesByIndex: Map, + indexValue: string | number, + value: number +) => { + const currentIndexValue = totalsPositivesByIndex.get(indexValue) || 0 + totalsPositivesByIndex.set(indexValue, currentIndexValue + (value > 0 ? value : 0)) +} + +export const updateGreatestValueByIndex = ( + greatestValueByIndex: Map, + indexValue: string | number, + value: number +) => { + const currentGreatestValue = greatestValueByIndex.get(indexValue) || 0 + greatestValueByIndex.set(indexValue, Math.max(currentGreatestValue, Number(value))) +} + +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/index.ts b/packages/bar/src/index.ts index 179dd8da6b..7efb63db8a 100644 --- a/packages/bar/src/index.ts +++ b/packages/bar/src/index.ts @@ -4,5 +4,6 @@ export * from './BarTooltip' export * from './BarCanvas' export * from './ResponsiveBar' export * from './ResponsiveBarCanvas' +export * from './BarTotals' export * from './props' export * from './types' diff --git a/packages/bar/src/props.ts b/packages/bar/src/props.ts index e069933c7b..d0033a96fd 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' @@ -51,7 +51,7 @@ export const defaultProps = { export const svgDefaultProps = { ...defaultProps, - layers: ['grid', 'axes', 'bars', 'markers', 'legends', 'annotations'], + layers: ['grid', 'axes', 'bars', 'markers', 'legends', 'annotations'] as BarLayerId[], barComponent: BarItem, defs: [], @@ -66,7 +66,7 @@ export const svgDefaultProps = { export const canvasDefaultProps = { ...defaultProps, - layers: ['grid', 'axes', 'bars', 'legends', 'annotations'], + layers: ['grid', 'axes', 'bars', '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 24e8ea52ea..0d7c37b236 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 diff --git a/storybook/stories/bar/Bar.stories.tsx b/storybook/stories/bar/Bar.stories.tsx index aa2a3b8326..2b1f7602bc 100644 --- a/storybook/stories/bar/Bar.stories.tsx +++ b/storybook/stories/bar/Bar.stories.tsx @@ -294,6 +294,15 @@ export const WithSymlogScale: Story = { ), } +export const WithTotals: Story = { + render: () => ( + + ), +} + const DataGenerator = (initialIndex, initialState) => { let index = initialIndex let state = initialState From b3a81c7386c116ed2f3725e0450b8fac1fe76b68 Mon Sep 17 00:00:00 2001 From: joaopedromatias Date: Sat, 2 Mar 2024 18:18:25 -0300 Subject: [PATCH 02/28] fix: insert unique key prop to each total --- packages/bar/src/BarTotals.tsx | 2 ++ packages/bar/tests/Bar.test.tsx | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/bar/src/BarTotals.tsx b/packages/bar/src/BarTotals.tsx index e3f95d918a..d6b32515c2 100644 --- a/packages/bar/src/BarTotals.tsx +++ b/packages/bar/src/BarTotals.tsx @@ -58,6 +58,7 @@ export const BarTotals = ({ totals.push( ({ totals.push( Date: Sat, 2 Mar 2024 18:53:06 -0300 Subject: [PATCH 03/28] test: insert tests to totals bar layer --- packages/bar/tests/Bar.test.tsx | 125 +++++++++++++++++++++++++++++++- 1 file changed, 124 insertions(+), 1 deletion(-) diff --git a/packages/bar/tests/Bar.test.tsx b/packages/bar/tests/Bar.test.tsx index 8a967478c9..88044801fc 100644 --- a/packages/bar/tests/Bar.test.tsx +++ b/packages/bar/tests/Bar.test.tsx @@ -1,5 +1,5 @@ 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, BarTotals } from '../' @@ -614,6 +614,129 @@ 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) + + expect(totals.children).toHaveLength(3) + ;(totals.children as ReactTestInstance[]).forEach((total, index) => { + if (index === 0) { + expect(total.findByType('text').children[0]).toBe(`2`) + } else if (index === 1) { + expect(total.findByType('text').children[0]).toBe(`3`) + } else if (index === 2) { + expect(total.findByType('text').children[0]).toBe(`4`) + } + }) + }) + it('should have the total text for each index with horizontal layout', () => { + const instance = create( + + ).root + + const totals = instance.findByType(BarTotals) + + expect(totals.children).toHaveLength(3) + ;(totals.children as ReactTestInstance[]).forEach((total, index) => { + if (index === 0) { + expect(total.findByType('text').children[0]).toBe(`2`) + } else if (index === 1) { + expect(total.findByType('text').children[0]).toBe(`4`) + } else if (index === 2) { + expect(total.findByType('text').children[0]).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) + + expect(totals.children).toHaveLength(3) + ;(totals.children as ReactTestInstance[]).forEach((total, index) => { + if (index === 0) { + expect(total.findByType('text').children[0]).toBe(`-2`) + } else if (index === 1) { + expect(total.findByType('text').children[0]).toBe(`-4`) + } else if (index === 2) { + expect(total.findByType('text').children[0]).toBe(`-6`) + } + }) + }) + 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) + + expect(totals.children).toHaveLength(3) + ;(totals.children as ReactTestInstance[]).forEach((total, index) => { + if (index === 0) { + expect(total.findByType('text').children[0]).toBe(`0`) + } else if (index === 1) { + expect(total.findByType('text').children[0]).toBe(`1`) + } else if (index === 2) { + expect(total.findByType('text').children[0]).toBe(`3`) + } + }) + }) +}) + describe('tooltip', () => { it('should render a tooltip when hovering a slice', () => { let component: ReactTestRenderer From 418f871b652679d3bdc94c3f300646fba1a093bd Mon Sep 17 00:00:00 2001 From: joaopedromatias Date: Sat, 2 Mar 2024 19:29:04 -0300 Subject: [PATCH 04/28] docs: insert recipe on website bar page --- website/src/data/components/bar/meta.yml | 134 ++++++++++++----------- 1 file changed, 68 insertions(+), 66 deletions(-) diff --git a/website/src/data/components/bar/meta.yml b/website/src/data/components/bar/meta.yml index 6af1929ba8..a313fe486f 100644 --- a/website/src/data/components/bar/meta.yml +++ b/website/src/data/components/bar/meta.yml @@ -1,76 +1,78 @@ flavors: - - flavor: svg - path: /bar/ - - flavor: canvas - path: /bar/canvas/ - - flavor: api - path: /bar/api/ + - flavor: svg + path: /bar/ + - flavor: canvas + path: /bar/canvas/ + - flavor: api + path: /bar/api/ Bar: - package: '@nivo/bar' - tags: [] - stories: - - label: Using markers - link: bar--with-marker - - label: Stacked diverging bar chart - link: bar--diverging-stacked - - label: Grouped diverging bar chart - link: bar--diverging-grouped - - label: Custom bar element - link: bar--custom-bar-item - - label: Formatting values - link: bar--with-formatted-values - - label: Using custom tooltip - link: bar--custom-tooltip - - label: Custom axis ticks - link: bar--custom-axis-ticks - - label: With symlog scale - link: bar--with-symlog-scale - - label: Race bar chart - link: bar--race-chart - - label: Initial hidden ids - link: bar--initial-hidden-ids - - label: Using custom label for legends - link: bar--custom-legend-labels - - label: Using annotations - link: bar--with-annotations - 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). + package: '@nivo/bar' + tags: [] + stories: + - label: Using markers + link: bar--with-marker + - label: Stacked diverging bar chart + link: bar--diverging-stacked + - label: Grouped diverging bar chart + link: bar--diverging-grouped + - label: Custom bar element + link: bar--custom-bar-item + - label: Formatting values + link: bar--with-formatted-values + - label: Using custom tooltip + link: bar--custom-tooltip + - label: Custom axis ticks + link: bar--custom-axis-ticks + - label: With symlog scale + link: bar--with-symlog-scale + - label: Race bar chart + link: bar--race-chart + - label: Initial hidden ids + link: bar--initial-hidden-ids + - label: Using custom label for legends + 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 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`. + 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). + 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 - `legends` property: `dataFrom`, it defines how to compute - 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). + 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 + `legends` property: `dataFrom`, it defines how to compute + 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: - - canvas - stories: - - label: Using custom layer - link: barcanvas--custom-layer - - label: Using custom bar renderer - link: barcanvas--custom-bar-renderer - - label: Using annotations - link: barcanvas--with-annotations - description: | - 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. + package: '@nivo/bar' + tags: + - canvas + stories: + - label: Using custom layer + link: barcanvas--custom-layer + - label: Using custom bar renderer + link: barcanvas--custom-bar-renderer + - label: Using annotations + link: barcanvas--with-annotations + description: | + 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`. From fe284c135d45981a108ba814007a0397379d3f97 Mon Sep 17 00:00:00 2001 From: joaopedromatias Date: Mon, 4 Mar 2024 20:12:25 -0300 Subject: [PATCH 05/28] fix: update scale functions types --- packages/bar/src/BarCanvas.tsx | 5 +++-- packages/bar/src/BarTotals.tsx | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/bar/src/BarCanvas.tsx b/packages/bar/src/BarCanvas.tsx index 8f44138239..8cb3be47c6 100644 --- a/packages/bar/src/BarCanvas.tsx +++ b/packages/bar/src/BarCanvas.tsx @@ -39,6 +39,7 @@ import { updateTotalsByIndex, updateTotalsPositivesByIndex, } from './BarTotals' +import { AnyScale, ScaleBand } from '@nivo/scales' type InnerBarCanvasProps = Omit< BarCanvasProps, @@ -62,8 +63,8 @@ const isNumber = (value: unknown): value is number => typeof value === 'number' function renderTotalsToCanvas( ctx: CanvasRenderingContext2D, bars: ComputedBarDatum[], - xScale: (value: string | number) => number, - yScale: (value: string | number) => number, + xScale: ScaleBand | AnyScale, + yScale: ScaleBand | AnyScale, layout: BarCommonProps['layout'] = defaultProps.layout, groupMode: BarCommonProps['groupMode'] = defaultProps.groupMode ) { diff --git a/packages/bar/src/BarTotals.tsx b/packages/bar/src/BarTotals.tsx index d6b32515c2..51f006346e 100644 --- a/packages/bar/src/BarTotals.tsx +++ b/packages/bar/src/BarTotals.tsx @@ -1,10 +1,11 @@ +import { AnyScale, ScaleBand } from '@nivo/scales' import { defaultProps } from './props' import { BarCommonProps, BarDatum, ComputedBarDatum } from './types' interface BarTotalsProps { bars: ComputedBarDatum[] - xScale: (value: string | number) => number - yScale: (value: string | number) => number + xScale: ScaleBand | AnyScale + yScale: ScaleBand | AnyScale layout?: BarCommonProps['layout'] groupMode?: BarCommonProps['groupMode'] } From d632462efbeaacffea9b93bfc1c63bd2affd9f53 Mon Sep 17 00:00:00 2001 From: joaopedromatias Date: Mon, 4 Mar 2024 21:39:20 -0300 Subject: [PATCH 06/28] refactor: enable totals by prop instead of directly on layers --- packages/bar/src/Bar.tsx | 26 ++++++++++++-------------- packages/bar/src/BarCanvas.tsx | 9 +++++++-- packages/bar/src/props.ts | 2 ++ packages/bar/src/types.ts | 4 +++- packages/bar/tests/Bar.test.tsx | 8 ++++---- storybook/stories/bar/Bar.stories.tsx | 7 +------ 6 files changed, 29 insertions(+), 27 deletions(-) diff --git a/packages/bar/src/Bar.tsx b/packages/bar/src/Bar.tsx index e648801466..1c43dcff6f 100644 --- a/packages/bar/src/Bar.tsx +++ b/packages/bar/src/Bar.tsx @@ -103,6 +103,8 @@ const InnerBar = ({ barAriaDescribedBy, initialHiddenIds, + + enableTotals = svgDefaultProps.enableTotals, }: InnerBarProps) => { const { animate, config: springConfig } = useMotionConfig() const { outerWidth, outerHeight, margin, innerWidth, innerHeight } = useDimensions( @@ -284,7 +286,6 @@ const InnerBar = ({ grid: null, legends: null, markers: null, - totals: null, } if (layers.includes('annotations')) { @@ -364,19 +365,6 @@ const InnerBar = ({ ) } - if (layers.includes('totals')) { - layerById.totals = ( - - ) - } - const layerContext: BarCustomLayerProps = useMemo( () => ({ ...commonProps, @@ -435,6 +423,16 @@ const InnerBar = ({ return layerById?.[layer] ?? null })} + {enableTotals && ( + + )} ) } diff --git a/packages/bar/src/BarCanvas.tsx b/packages/bar/src/BarCanvas.tsx index 8cb3be47c6..5b0b80c0c3 100644 --- a/packages/bar/src/BarCanvas.tsx +++ b/packages/bar/src/BarCanvas.tsx @@ -261,6 +261,8 @@ const InnerBarCanvas = ({ pixelRatio = canvasDefaultProps.pixelRatio, canvasRef, + + enableTotals = canvasDefaultProps.enableTotals, }: InnerBarCanvasProps) => { const canvasEl = useRef(null) @@ -457,13 +459,15 @@ const InnerBarCanvas = ({ }) } else if (layer === 'annotations') { renderAnnotationsToCanvas(ctx, { annotations: boundAnnotations, theme }) - } else if (layer === 'totals') { - renderTotalsToCanvas(ctx, bars, xScale, yScale, layout, groupMode) } else if (typeof layer === 'function') { layer(ctx, layerContext) } }) + if (enableTotals) { + renderTotalsToCanvas(ctx, bars, xScale, yScale, layout, groupMode) + } + ctx.save() }, [ axisBottom, @@ -502,6 +506,7 @@ const InnerBarCanvas = ({ theme, width, bars, + enableTotals, ]) const handleMouseHover = useCallback( diff --git a/packages/bar/src/props.ts b/packages/bar/src/props.ts index d0033a96fd..667405d758 100644 --- a/packages/bar/src/props.ts +++ b/packages/bar/src/props.ts @@ -47,6 +47,8 @@ export const defaultProps = { initialHiddenIds: [], annotations: [], markers: [], + + enableTotals: false, } export const svgDefaultProps = { diff --git a/packages/bar/src/types.ts b/packages/bar/src/types.ts index 0d7c37b236..ff6d104e64 100644 --- a/packages/bar/src/types.ts +++ b/packages/bar/src/types.ts @@ -95,7 +95,7 @@ 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' | 'totals' +export type BarLayerId = 'grid' | 'axes' | 'bars' | 'markers' | 'legends' | 'annotations' export type BarCanvasLayerId = Exclude interface BarCustomLayerBaseProps @@ -258,6 +258,8 @@ export type BarCommonProps = { renderWrapper?: boolean initialHiddenIds: readonly (string | number)[] + + enableTotals?: boolean } export type BarSvgProps = Partial> & diff --git a/packages/bar/tests/Bar.test.tsx b/packages/bar/tests/Bar.test.tsx index 88044801fc..d88285ddb1 100644 --- a/packages/bar/tests/Bar.test.tsx +++ b/packages/bar/tests/Bar.test.tsx @@ -620,7 +620,7 @@ describe('totals layer', () => { { { { ( - - ), + render: () => , } const DataGenerator = (initialIndex, initialState) => { From a40f8cfaa97dda53f50949d5b907fa0eb498d536 Mon Sep 17 00:00:00 2001 From: joaopedromatias Date: Mon, 4 Mar 2024 22:03:56 -0300 Subject: [PATCH 07/28] style: apply theme configuration on totals --- packages/bar/src/Bar.tsx | 3 +++ packages/bar/src/BarCanvas.tsx | 11 ++++++----- packages/bar/src/BarTotals.tsx | 8 +++++--- packages/bar/tests/Bar.test.tsx | 31 +++++++++++++++++++++++++++++++ 4 files changed, 45 insertions(+), 8 deletions(-) diff --git a/packages/bar/src/Bar.tsx b/packages/bar/src/Bar.tsx index 1c43dcff6f..6f67e8ed5e 100644 --- a/packages/bar/src/Bar.tsx +++ b/packages/bar/src/Bar.tsx @@ -17,6 +17,7 @@ import { bindDefs, useDimensions, useMotionConfig, + useTheme, } from '@nivo/core' import { Fragment, ReactNode, createElement, useMemo } from 'react' import { svgDefaultProps } from './props' @@ -106,6 +107,7 @@ const InnerBar = ({ enableTotals = svgDefaultProps.enableTotals, }: InnerBarProps) => { + const theme = useTheme() const { animate, config: springConfig } = useMotionConfig() const { outerWidth, outerHeight, margin, innerWidth, innerHeight } = useDimensions( width, @@ -431,6 +433,7 @@ const InnerBar = ({ bars={bars} xScale={xScale} yScale={yScale} + theme={theme.text} /> )} diff --git a/packages/bar/src/BarCanvas.tsx b/packages/bar/src/BarCanvas.tsx index 5b0b80c0c3..8235a4c859 100644 --- a/packages/bar/src/BarCanvas.tsx +++ b/packages/bar/src/BarCanvas.tsx @@ -9,6 +9,7 @@ import { import { Container, Margin, + TextStyle, getRelativeCursor, isCursorInRect, useDimensions, @@ -66,7 +67,8 @@ function renderTotalsToCanvas( xScale: ScaleBand | AnyScale, yScale: ScaleBand | AnyScale, layout: BarCommonProps['layout'] = defaultProps.layout, - groupMode: BarCommonProps['groupMode'] = defaultProps.groupMode + groupMode: BarCommonProps['groupMode'] = defaultProps.groupMode, + theme: TextStyle ) { if (bars.length === 0) return @@ -76,10 +78,9 @@ function renderTotalsToCanvas( const barHeight = bars[0].height const yOffsetVertically = -10 const xOffsetHorizontally = 20 - const fontSize = 12 - ctx.fillStyle = '#222222' - ctx.font = `${fontSize}px sans-serif` + ctx.fillStyle = theme.fill + ctx.font = `bold ${theme.fontSize}px sans-serif` ctx.textBaseline = 'middle' ctx.textAlign = 'center' @@ -465,7 +466,7 @@ const InnerBarCanvas = ({ }) if (enableTotals) { - renderTotalsToCanvas(ctx, bars, xScale, yScale, layout, groupMode) + renderTotalsToCanvas(ctx, bars, xScale, yScale, layout, groupMode, theme.text) } ctx.save() diff --git a/packages/bar/src/BarTotals.tsx b/packages/bar/src/BarTotals.tsx index 51f006346e..6302a17dd1 100644 --- a/packages/bar/src/BarTotals.tsx +++ b/packages/bar/src/BarTotals.tsx @@ -1,6 +1,7 @@ import { AnyScale, ScaleBand } from '@nivo/scales' import { defaultProps } from './props' import { BarCommonProps, BarDatum, ComputedBarDatum } from './types' +import { TextStyle } from '@nivo/core' interface BarTotalsProps { bars: ComputedBarDatum[] @@ -8,6 +9,7 @@ interface BarTotalsProps { yScale: ScaleBand | AnyScale layout?: BarCommonProps['layout'] groupMode?: BarCommonProps['groupMode'] + theme: TextStyle } export const BarTotals = ({ @@ -16,6 +18,7 @@ export const BarTotals = ({ yScale, layout = defaultProps.layout, groupMode = defaultProps.groupMode, + theme, }: BarTotalsProps) => { if (bars.length === 0) return <> const totals: JSX.Element[] = [] @@ -26,12 +29,11 @@ export const BarTotals = ({ const barHeight = bars[0].height const yOffsetVertically = -10 const xOffsetHorizontally = 20 - const fontSize = 12 const commonProps = { - fill: '#222222', + fill: theme.fill, fontWeight: 'bold', - fontSize, + fontSize: theme.fontSize, textAnchor: 'middle', alignmentBaseline: 'middle', } as const diff --git a/packages/bar/tests/Bar.test.tsx b/packages/bar/tests/Bar.test.tsx index d88285ddb1..c64058a582 100644 --- a/packages/bar/tests/Bar.test.tsx +++ b/packages/bar/tests/Bar.test.tsx @@ -735,6 +735,37 @@ describe('totals layer', () => { } }) }) + it('should follow the theme configurations', () => { + const instance = create( + + ).root + + const totals = instance.findByType(BarTotals) + + expect(totals.children).toHaveLength(3) + ;(totals.children as ReactTestInstance[]).forEach((total, index) => { + const props = total.findByType('text').props + expect(props.fill).toBe('red') + expect(props.fontSize).toBe(14) + }) + }) }) describe('tooltip', () => { From 0b97fcbcd14870920a1d77833f707e591d85de22 Mon Sep 17 00:00:00 2001 From: joaopedromatias Date: Mon, 4 Mar 2024 22:15:28 -0300 Subject: [PATCH 08/28] feat: make totals offsets configurable --- packages/bar/src/Bar.tsx | 2 ++ packages/bar/src/BarCanvas.tsx | 30 +++++++++++++++++---------- packages/bar/src/BarTotals.tsx | 22 +++++++------------- packages/bar/src/props.ts | 1 + packages/bar/src/types.ts | 1 + storybook/stories/bar/Bar.stories.tsx | 2 +- 6 files changed, 31 insertions(+), 27 deletions(-) diff --git a/packages/bar/src/Bar.tsx b/packages/bar/src/Bar.tsx index 6f67e8ed5e..1a6a6c5cb4 100644 --- a/packages/bar/src/Bar.tsx +++ b/packages/bar/src/Bar.tsx @@ -106,6 +106,7 @@ const InnerBar = ({ initialHiddenIds, enableTotals = svgDefaultProps.enableTotals, + totalsOffset = svgDefaultProps.totalsOffset, }: InnerBarProps) => { const theme = useTheme() const { animate, config: springConfig } = useMotionConfig() @@ -434,6 +435,7 @@ const InnerBar = ({ xScale={xScale} yScale={yScale} theme={theme.text} + totalsOffset={totalsOffset} /> )} diff --git a/packages/bar/src/BarCanvas.tsx b/packages/bar/src/BarCanvas.tsx index 8235a4c859..04be2b5595 100644 --- a/packages/bar/src/BarCanvas.tsx +++ b/packages/bar/src/BarCanvas.tsx @@ -68,7 +68,8 @@ function renderTotalsToCanvas( yScale: ScaleBand | AnyScale, layout: BarCommonProps['layout'] = defaultProps.layout, groupMode: BarCommonProps['groupMode'] = defaultProps.groupMode, - theme: TextStyle + theme: TextStyle, + totalsOffset: number ) { if (bars.length === 0) return @@ -76,8 +77,6 @@ function renderTotalsToCanvas( const barWidth = bars[0].width const barHeight = bars[0].height - const yOffsetVertically = -10 - const xOffsetHorizontally = 20 ctx.fillStyle = theme.fill ctx.font = `bold ${theme.fontSize}px sans-serif` @@ -107,13 +106,11 @@ function renderTotalsToCanvas( ctx.fillText( String(totalsByIndex.get(indexValue)), - xPosition + (layout === 'vertical' ? barWidth / 2 : xOffsetHorizontally), - yPosition + (layout === 'vertical' ? yOffsetVertically : barHeight / 2) + xPosition + (layout === 'vertical' ? barWidth / 2 : totalsOffset), + yPosition + (layout === 'vertical' ? -totalsOffset : barHeight / 2) ) }) - } - - if (groupMode === 'grouped') { + } else if (groupMode === 'grouped') { const greatestValueByIndex = new Map() const numberOfBarsByIndex = new Map() @@ -141,8 +138,8 @@ function renderTotalsToCanvas( ctx.fillText( String(totalsByIndex.get(indexValue)), - xPosition + (layout === 'vertical' ? indexBarsWidth / 2 : xOffsetHorizontally), - yPosition + (layout === 'vertical' ? yOffsetVertically : indexBarsHeight / 2) + xPosition + (layout === 'vertical' ? indexBarsWidth / 2 : totalsOffset), + yPosition + (layout === 'vertical' ? -totalsOffset : indexBarsHeight / 2) ) }) } @@ -264,6 +261,7 @@ const InnerBarCanvas = ({ canvasRef, enableTotals = canvasDefaultProps.enableTotals, + totalsOffset = canvasDefaultProps.totalsOffset, }: InnerBarCanvasProps) => { const canvasEl = useRef(null) @@ -466,7 +464,16 @@ const InnerBarCanvas = ({ }) if (enableTotals) { - renderTotalsToCanvas(ctx, bars, xScale, yScale, layout, groupMode, theme.text) + renderTotalsToCanvas( + ctx, + bars, + xScale, + yScale, + layout, + groupMode, + theme.text, + totalsOffset + ) } ctx.save() @@ -508,6 +515,7 @@ const InnerBarCanvas = ({ width, bars, enableTotals, + totalsOffset, ]) const handleMouseHover = useCallback( diff --git a/packages/bar/src/BarTotals.tsx b/packages/bar/src/BarTotals.tsx index 6302a17dd1..2a958d73af 100644 --- a/packages/bar/src/BarTotals.tsx +++ b/packages/bar/src/BarTotals.tsx @@ -10,6 +10,7 @@ interface BarTotalsProps { layout?: BarCommonProps['layout'] groupMode?: BarCommonProps['groupMode'] theme: TextStyle + totalsOffset: number } export const BarTotals = ({ @@ -19,6 +20,7 @@ export const BarTotals = ({ layout = defaultProps.layout, groupMode = defaultProps.groupMode, theme, + totalsOffset, }: BarTotalsProps) => { if (bars.length === 0) return <> const totals: JSX.Element[] = [] @@ -27,8 +29,6 @@ export const BarTotals = ({ const barWidth = bars[0].width const barHeight = bars[0].height - const yOffsetVertically = -10 - const xOffsetHorizontally = 20 const commonProps = { fill: theme.fill, @@ -62,17 +62,15 @@ export const BarTotals = ({ totals.push( {totalsByIndex.get(indexValue)} ) }) - } - - if (groupMode === 'grouped') { + } else if (groupMode === 'grouped') { const greatestValueByIndex = new Map() const numberOfBarsByIndex = new Map() @@ -101,14 +99,8 @@ export const BarTotals = ({ totals.push( {totalsByIndex.get(indexValue)} diff --git a/packages/bar/src/props.ts b/packages/bar/src/props.ts index 667405d758..08d4c3d6dc 100644 --- a/packages/bar/src/props.ts +++ b/packages/bar/src/props.ts @@ -49,6 +49,7 @@ export const defaultProps = { markers: [], enableTotals: false, + totalsOffset: 10, } export const svgDefaultProps = { diff --git a/packages/bar/src/types.ts b/packages/bar/src/types.ts index ff6d104e64..a225036bf6 100644 --- a/packages/bar/src/types.ts +++ b/packages/bar/src/types.ts @@ -260,6 +260,7 @@ export type BarCommonProps = { initialHiddenIds: readonly (string | number)[] enableTotals?: boolean + totalsOffset?: number } export type BarSvgProps = Partial> & diff --git a/storybook/stories/bar/Bar.stories.tsx b/storybook/stories/bar/Bar.stories.tsx index b618ab566d..2c9ddb5f6f 100644 --- a/storybook/stories/bar/Bar.stories.tsx +++ b/storybook/stories/bar/Bar.stories.tsx @@ -295,7 +295,7 @@ export const WithSymlogScale: Story = { } export const WithTotals: Story = { - render: () => , + render: () => , } const DataGenerator = (initialIndex, initialState) => { From 15beb019bfaa80b873aecd341fcc62fa5ae13611 Mon Sep 17 00:00:00 2001 From: joaopedromatias Date: Tue, 5 Mar 2024 00:19:34 -0300 Subject: [PATCH 09/28] refactor: centralize compute of totals bar --- packages/bar/src/Bar.tsx | 39 ++++-- packages/bar/src/BarCanvas.tsx | 126 +++--------------- .../src/{BarTotals.tsx => compute/totals.ts} | 100 +++++++------- packages/bar/src/index.ts | 1 - packages/bar/tests/Bar.test.tsx | 80 ++++++----- 5 files changed, 137 insertions(+), 209 deletions(-) rename packages/bar/src/{BarTotals.tsx => compute/totals.ts} (61%) diff --git a/packages/bar/src/Bar.tsx b/packages/bar/src/Bar.tsx index 1a6a6c5cb4..cdd0dd383e 100644 --- a/packages/bar/src/Bar.tsx +++ b/packages/bar/src/Bar.tsx @@ -23,7 +23,7 @@ import { Fragment, ReactNode, createElement, useMemo } from 'react' import { svgDefaultProps } from './props' import { useTransition } from '@react-spring/web' import { useBar } from './hooks' -import { BarTotals } from './BarTotals' +import { computeBarTotals } from './compute/totals' type InnerBarProps = Omit< BarSvgProps, @@ -407,6 +407,16 @@ const InnerBar = ({ ] ) + const barTotals = computeBarTotals( + enableTotals, + bars, + xScale, + yScale, + layout, + groupMode, + totalsOffset + ) + return ( ({ return layerById?.[layer] ?? null })} - {enableTotals && ( - - )} + {barTotals.map(barTotal => ( + + {barTotal.value} + + ))} ) } diff --git a/packages/bar/src/BarCanvas.tsx b/packages/bar/src/BarCanvas.tsx index 04be2b5595..43cc7fe2cd 100644 --- a/packages/bar/src/BarCanvas.tsx +++ b/packages/bar/src/BarCanvas.tsx @@ -2,14 +2,12 @@ import { BarCanvasCustomLayerProps, BarCanvasLayer, BarCanvasProps, - BarCommonProps, BarDatum, ComputedBarDatum, } from './types' import { Container, Margin, - TextStyle, getRelativeCursor, isCursorInRect, useDimensions, @@ -24,7 +22,7 @@ import { useMemo, useRef, } from 'react' -import { canvasDefaultProps, defaultProps } from './props' +import { canvasDefaultProps } from './props' import { renderAnnotationsToCanvas, useAnnotations, @@ -34,13 +32,7 @@ import { renderAxesToCanvas, renderGridLinesToCanvas } from '@nivo/axes' import { renderLegendToCanvas } from '@nivo/legends' import { useTooltip } from '@nivo/tooltip' import { useBar } from './hooks' -import { - updateGreatestValueByIndex, - updateNumberOfBarsByIndex, - updateTotalsByIndex, - updateTotalsPositivesByIndex, -} from './BarTotals' -import { AnyScale, ScaleBand } from '@nivo/scales' +import { computeBarTotals } from './compute/totals' type InnerBarCanvasProps = Omit< BarCanvasProps, @@ -61,90 +53,6 @@ const findBarUnderCursor = ( const isNumber = (value: unknown): value is number => typeof value === 'number' -function renderTotalsToCanvas( - ctx: CanvasRenderingContext2D, - bars: ComputedBarDatum[], - xScale: ScaleBand | AnyScale, - yScale: ScaleBand | AnyScale, - layout: BarCommonProps['layout'] = defaultProps.layout, - groupMode: BarCommonProps['groupMode'] = defaultProps.groupMode, - theme: TextStyle, - totalsOffset: number -) { - if (bars.length === 0) return - - const totalsByIndex = new Map() - - const barWidth = bars[0].width - const barHeight = bars[0].height - - ctx.fillStyle = theme.fill - ctx.font = `bold ${theme.fontSize}px sans-serif` - ctx.textBaseline = 'middle' - ctx.textAlign = 'center' - - 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) => { - let xPosition: number - let yPosition: number - - if (layout === 'vertical') { - xPosition = xScale(indexValue) - yPosition = yScale(totalsPositive) - } else { - xPosition = xScale(totalsPositive) - yPosition = yScale(indexValue) - } - - ctx.fillText( - String(totalsByIndex.get(indexValue)), - xPosition + (layout === 'vertical' ? barWidth / 2 : totalsOffset), - yPosition + (layout === 'vertical' ? -totalsOffset : barHeight / 2) - ) - }) - } 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) => { - let xPosition: number - let yPosition: number - - if (layout === 'vertical') { - xPosition = xScale(indexValue) - yPosition = yScale(greatestValue) - } else { - xPosition = xScale(greatestValue) - yPosition = yScale(indexValue) - } - - const indexBarsWidth = numberOfBarsByIndex.get(indexValue) * barWidth - const indexBarsHeight = numberOfBarsByIndex.get(indexValue) * barHeight - - ctx.fillText( - String(totalsByIndex.get(indexValue)), - xPosition + (layout === 'vertical' ? indexBarsWidth / 2 : totalsOffset), - yPosition + (layout === 'vertical' ? -totalsOffset : indexBarsHeight / 2) - ) - }) - } -} - const InnerBarCanvas = ({ data, indexBy, @@ -463,18 +371,24 @@ const InnerBarCanvas = ({ } }) - if (enableTotals) { - renderTotalsToCanvas( - ctx, - bars, - xScale, - yScale, - layout, - groupMode, - theme.text, - totalsOffset - ) - } + const barTotals = computeBarTotals( + enableTotals, + bars, + xScale, + yScale, + layout, + groupMode, + totalsOffset + ) + + ctx.fillStyle = theme.text.fill + ctx.font = `bold ${theme.text.fontSize}px sans-serif` + ctx.textBaseline = 'middle' + ctx.textAlign = 'center' + + barTotals.forEach(barTotal => { + ctx.fillText(String(barTotal.value), barTotal.x, barTotal.y) + }) ctx.save() }, [ diff --git a/packages/bar/src/BarTotals.tsx b/packages/bar/src/compute/totals.ts similarity index 61% rename from packages/bar/src/BarTotals.tsx rename to packages/bar/src/compute/totals.ts index 2a958d73af..e3e4724ef1 100644 --- a/packages/bar/src/BarTotals.tsx +++ b/packages/bar/src/compute/totals.ts @@ -1,43 +1,32 @@ import { AnyScale, ScaleBand } from '@nivo/scales' -import { defaultProps } from './props' -import { BarCommonProps, BarDatum, ComputedBarDatum } from './types' -import { TextStyle } from '@nivo/core' - -interface BarTotalsProps { - bars: ComputedBarDatum[] - xScale: ScaleBand | AnyScale - yScale: ScaleBand | AnyScale - layout?: BarCommonProps['layout'] - groupMode?: BarCommonProps['groupMode'] - theme: TextStyle - totalsOffset: number +import { defaultProps } from '../props' +import { BarCommonProps, BarDatum, ComputedBarDatum } from '../types' + +interface BarTotalsData { + key: string + x: number + y: number + value: number } -export const BarTotals = ({ - bars, - xScale, - yScale, - layout = defaultProps.layout, - groupMode = defaultProps.groupMode, - theme, - totalsOffset, -}: BarTotalsProps) => { - if (bars.length === 0) return <> - const totals: JSX.Element[] = [] +export const computeBarTotals = ( + enableTotals: boolean, + bars: ComputedBarDatum[], + xScale: ScaleBand | AnyScale, + yScale: ScaleBand | AnyScale, + layout: BarCommonProps['layout'] = defaultProps.layout, + groupMode: BarCommonProps['groupMode'] = defaultProps.groupMode, + totalsOffset: number +) => { + const totals = [] as BarTotalsData[] + + if (!enableTotals || bars.length === 0) return totals const totalsByIndex = new Map() const barWidth = bars[0].width const barHeight = bars[0].height - const commonProps = { - fill: theme.fill, - fontWeight: 'bold', - fontSize: theme.fontSize, - textAnchor: 'middle', - alignmentBaseline: 'middle', - } as const - if (groupMode === 'stacked') { const totalsPositivesByIndex = new Map() @@ -48,6 +37,8 @@ export const BarTotals = ({ }) totalsPositivesByIndex.forEach((totalsPositive, indexValue) => { + const indexTotal = totalsByIndex.get(indexValue) || 0 + let xPosition: number let yPosition: number @@ -59,16 +50,15 @@ export const BarTotals = ({ yPosition = yScale(indexValue) } - totals.push( - - {totalsByIndex.get(indexValue)} - - ) + xPosition += layout === 'vertical' ? barWidth / 2 : totalsOffset + yPosition += layout === 'vertical' ? -totalsOffset : barHeight / 2 + + totals.push({ + key: 'total_' + indexValue, + x: xPosition, + y: yPosition, + value: indexTotal, + }) }) } else if (groupMode === 'grouped') { const greatestValueByIndex = new Map() @@ -82,6 +72,8 @@ export const BarTotals = ({ }) greatestValueByIndex.forEach((greatestValue, indexValue) => { + const indexTotal = totalsByIndex.get(indexValue) || 0 + let xPosition: number let yPosition: number @@ -96,22 +88,21 @@ export const BarTotals = ({ const indexBarsWidth = numberOfBarsByIndex.get(indexValue) * barWidth const indexBarsHeight = numberOfBarsByIndex.get(indexValue) * barHeight - totals.push( - - {totalsByIndex.get(indexValue)} - - ) + xPosition += layout === 'vertical' ? indexBarsWidth / 2 : totalsOffset + yPosition += layout === 'vertical' ? -totalsOffset : indexBarsHeight / 2 + + totals.push({ + key: 'total_' + indexValue, + x: xPosition, + y: yPosition, + value: indexTotal, + }) }) } - - return <>{totals} + 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, @@ -121,6 +112,7 @@ export const updateTotalsByIndex = ( 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, @@ -130,6 +122,7 @@ export const updateTotalsPositivesByIndex = ( 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, @@ -139,6 +132,7 @@ export const updateGreatestValueByIndex = ( 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 diff --git a/packages/bar/src/index.ts b/packages/bar/src/index.ts index 7efb63db8a..179dd8da6b 100644 --- a/packages/bar/src/index.ts +++ b/packages/bar/src/index.ts @@ -4,6 +4,5 @@ export * from './BarTooltip' export * from './BarCanvas' export * from './ResponsiveBar' export * from './ResponsiveBarCanvas' -export * from './BarTotals' export * from './props' export * from './types' diff --git a/packages/bar/tests/Bar.test.tsx b/packages/bar/tests/Bar.test.tsx index c64058a582..38508a8369 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, type ReactTestInstance } from 'react-test-renderer' import { LegendSvg, LegendSvgItem } from '@nivo/legends' -import { Bar, BarDatum, BarItemProps, ComputedDatum, BarItem, BarTooltip, BarTotals } from '../' +import { Bar, BarDatum, BarItemProps, ComputedDatum, BarItem, BarTooltip } from '../' type IdValue = { id: string @@ -621,26 +621,28 @@ describe('totals layer', () => { width={500} height={300} enableTotals={true} - keys={['value1', 'value2']} + keys={['costA', 'costB']} data={[ - { id: 'one', value1: 1, value2: 1 }, - { id: 'two', value1: 2, value2: 1 }, - { id: 'three', value1: 3, value2: 1 }, + { id: 'one', costA: 1, costB: 1 }, + { id: 'two', costA: 2, costB: 1 }, + { id: 'three', costA: 3, costB: 1 }, ]} animate={false} /> ).root - const totals = instance.findByType(BarTotals) + const totals = instance.findAllByType('text').filter(text => { + return text.props['data-test'] === 'bar-total' + }) - expect(totals.children).toHaveLength(3) - ;(totals.children as ReactTestInstance[]).forEach((total, index) => { + expect(totals).toHaveLength(3) + totals.forEach((total, index) => { if (index === 0) { - expect(total.findByType('text').children[0]).toBe(`2`) + expect(total.children[0]).toBe(`2`) } else if (index === 1) { - expect(total.findByType('text').children[0]).toBe(`3`) + expect(total.children[0]).toBe(`3`) } else if (index === 2) { - expect(total.findByType('text').children[0]).toBe(`4`) + expect(total.children[0]).toBe(`4`) } }) }) @@ -661,16 +663,18 @@ describe('totals layer', () => { /> ).root - const totals = instance.findByType(BarTotals) + const totals = instance.findAllByType('text').filter(text => { + return text.props['data-test'] === 'bar-total' + }) - expect(totals.children).toHaveLength(3) - ;(totals.children as ReactTestInstance[]).forEach((total, index) => { + expect(totals).toHaveLength(3) + totals.forEach((total, index) => { if (index === 0) { - expect(total.findByType('text').children[0]).toBe(`2`) + expect(total.children[0]).toBe(`2`) } else if (index === 1) { - expect(total.findByType('text').children[0]).toBe(`4`) + expect(total.children[0]).toBe(`4`) } else if (index === 2) { - expect(total.findByType('text').children[0]).toBe(`6`) + expect(total.children[0]).toBe(`6`) } }) }) @@ -685,22 +689,21 @@ describe('totals layer', () => { data={[ { id: 'one', value1: -1, value2: -1 }, { id: 'two', value1: -2, value2: -2 }, - { id: 'three', value1: -3, value2: -3 }, ]} animate={false} /> ).root - const totals = instance.findByType(BarTotals) + const totals = instance.findAllByType('text').filter(text => { + return text.props['data-test'] === 'bar-total' + }) - expect(totals.children).toHaveLength(3) - ;(totals.children as ReactTestInstance[]).forEach((total, index) => { + expect(totals).toHaveLength(2) + totals.forEach((total, index) => { if (index === 0) { - expect(total.findByType('text').children[0]).toBe(`-2`) - } else if (index === 1) { - expect(total.findByType('text').children[0]).toBe(`-4`) - } else if (index === 2) { - expect(total.findByType('text').children[0]).toBe(`-6`) + expect(total.children[0]).toBe(`-2`) + } else { + expect(total.children[0]).toBe(`-4`) } }) }) @@ -722,16 +725,19 @@ describe('totals layer', () => { /> ).root - const totals = instance.findByType(BarTotals) + const totals = instance.findAllByType('text').filter(text => { + return text.props['data-test'] === 'bar-total' + }) + + expect(totals).toHaveLength(3) - expect(totals.children).toHaveLength(3) - ;(totals.children as ReactTestInstance[]).forEach((total, index) => { + totals.forEach((total, index) => { if (index === 0) { - expect(total.findByType('text').children[0]).toBe(`0`) + expect(total.children[0]).toBe(`0`) } else if (index === 1) { - expect(total.findByType('text').children[0]).toBe(`1`) + expect(total.children[0]).toBe(`1`) } else if (index === 2) { - expect(total.findByType('text').children[0]).toBe(`3`) + expect(total.children[0]).toBe(`3`) } }) }) @@ -757,11 +763,13 @@ describe('totals layer', () => { /> ).root - const totals = instance.findByType(BarTotals) + const totals = instance.findAllByType('text').filter(text => { + return text.props['data-test'] === 'bar-total' + }) - expect(totals.children).toHaveLength(3) - ;(totals.children as ReactTestInstance[]).forEach((total, index) => { - const props = total.findByType('text').props + expect(totals).toHaveLength(3) + totals.forEach((total, index) => { + const props = total.props expect(props.fill).toBe('red') expect(props.fontSize).toBe(14) }) From 7bea4a1de92afd98d2e2c5a70b0062e0a3e9400a Mon Sep 17 00:00:00 2001 From: joaopedromatias Date: Tue, 5 Mar 2024 00:25:18 -0300 Subject: [PATCH 10/28] chore: re-format website docs yml --- website/src/data/components/bar/meta.yml | 142 +++++++++++------------ 1 file changed, 68 insertions(+), 74 deletions(-) diff --git a/website/src/data/components/bar/meta.yml b/website/src/data/components/bar/meta.yml index a313fe486f..989f3706c5 100644 --- a/website/src/data/components/bar/meta.yml +++ b/website/src/data/components/bar/meta.yml @@ -1,78 +1,72 @@ flavors: - - flavor: svg - path: /bar/ - - flavor: canvas - path: /bar/canvas/ - - flavor: api - path: /bar/api/ + - flavor: svg + path: /bar/ + - flavor: canvas + path: /bar/canvas/ + - flavor: api + path: /bar/api/ Bar: - package: '@nivo/bar' - tags: [] - stories: - - label: Using markers - link: bar--with-marker - - label: Stacked diverging bar chart - link: bar--diverging-stacked - - label: Grouped diverging bar chart - link: bar--diverging-grouped - - label: Custom bar element - link: bar--custom-bar-item - - label: Formatting values - link: bar--with-formatted-values - - label: Using custom tooltip - link: bar--custom-tooltip - - label: Custom axis ticks - link: bar--custom-axis-ticks - - label: With symlog scale - link: bar--with-symlog-scale - - label: Race bar chart - link: bar--race-chart - - label: Initial hidden ids - link: bar--initial-hidden-ids - - label: Using custom label for legends - 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 - `legends` property: `dataFrom`, it defines how to compute - 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). - + package: '@nivo/bar' + tags: [] + stories: + - label: Using markers + link: bar--with-marker + - label: Stacked diverging bar chart + link: bar--diverging-stacked + - label: Grouped diverging bar chart + link: bar--diverging-grouped + - label: Custom bar element + link: bar--custom-bar-item + - label: Formatting values + link: bar--with-formatted-values + - label: Using custom tooltip + link: bar--custom-tooltip + - label: Custom axis ticks + link: bar--custom-axis-ticks + - label: With symlog scale + link: bar--with-symlog-scale + - label: Race bar chart + link: bar--race-chart + - label: Initial hidden ids + link: bar--initial-hidden-ids + - label: Using custom label for legends + 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 + `legends` property: `dataFrom`, it defines how to compute + 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: - - canvas - stories: - - label: Using custom layer - link: barcanvas--custom-layer - - label: Using custom bar renderer - link: barcanvas--custom-bar-renderer - - label: Using annotations - link: barcanvas--with-annotations - description: | - 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`. + package: '@nivo/bar' + tags: + - canvas + stories: + - label: Using custom layer + link: barcanvas--custom-layer + - label: Using custom bar renderer + link: barcanvas--custom-bar-renderer + - label: Using annotations + link: barcanvas--with-annotations + description: | + 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`. \ No newline at end of file From eeefd6216f076c9f074835ce7dd221a95d7b8f30 Mon Sep 17 00:00:00 2001 From: joaopedromatias Date: Tue, 5 Mar 2024 19:41:52 -0300 Subject: [PATCH 11/28] refactor: use props along with layers to enable totals --- packages/bar/src/Bar.tsx | 32 +++++-------------- packages/bar/src/BarCanvas.tsx | 47 ++++++++++++++++------------ packages/bar/src/BarTotals.tsx | 29 +++++++++++++++++ packages/bar/src/compute/totals.ts | 5 ++- packages/bar/src/index.ts | 1 + packages/bar/src/props.ts | 4 +-- packages/bar/src/types.ts | 2 +- packages/bar/tests/Bar.test.tsx | 50 +++++++++++------------------- 8 files changed, 87 insertions(+), 83 deletions(-) create mode 100644 packages/bar/src/BarTotals.tsx diff --git a/packages/bar/src/Bar.tsx b/packages/bar/src/Bar.tsx index cdd0dd383e..5f57be84a0 100644 --- a/packages/bar/src/Bar.tsx +++ b/packages/bar/src/Bar.tsx @@ -24,6 +24,7 @@ import { svgDefaultProps } from './props' import { useTransition } from '@react-spring/web' import { useBar } from './hooks' import { computeBarTotals } from './compute/totals' +import { BarTotals } from './BarTotals' type InnerBarProps = Omit< BarSvgProps, @@ -289,6 +290,7 @@ const InnerBar = ({ grid: null, legends: null, markers: null, + totals: null, } if (layers.includes('annotations')) { @@ -368,6 +370,11 @@ const InnerBar = ({ ) } + if (layers.includes('totals') && enableTotals) { + const barTotals = computeBarTotals(bars, xScale, yScale, layout, groupMode, totalsOffset) + layerById.totals = + } + const layerContext: BarCustomLayerProps = useMemo( () => ({ ...commonProps, @@ -407,16 +414,6 @@ const InnerBar = ({ ] ) - const barTotals = computeBarTotals( - enableTotals, - bars, - xScale, - yScale, - layout, - groupMode, - totalsOffset - ) - return ( ({ return layerById?.[layer] ?? null })} - {barTotals.map(barTotal => ( - - {barTotal.value} - - ))} ) } diff --git a/packages/bar/src/BarCanvas.tsx b/packages/bar/src/BarCanvas.tsx index 43cc7fe2cd..39b8e6ede9 100644 --- a/packages/bar/src/BarCanvas.tsx +++ b/packages/bar/src/BarCanvas.tsx @@ -6,6 +6,7 @@ import { ComputedBarDatum, } from './types' import { + CompleteTheme, Container, Margin, getRelativeCursor, @@ -32,7 +33,7 @@ import { renderAxesToCanvas, renderGridLinesToCanvas } from '@nivo/axes' import { renderLegendToCanvas } from '@nivo/legends' import { useTooltip } from '@nivo/tooltip' import { useBar } from './hooks' -import { computeBarTotals } from './compute/totals' +import { BarTotalsData, computeBarTotals } from './compute/totals' type InnerBarCanvasProps = Omit< BarCanvasProps, @@ -53,6 +54,21 @@ const findBarUnderCursor = ( const isNumber = (value: unknown): value is number => typeof value === 'number' +function renderTotalsToCanvas( + ctx: CanvasRenderingContext2D, + barTotals: BarTotalsData[], + theme: CompleteTheme +) { + ctx.fillStyle = theme.text.fill + ctx.font = `bold ${theme.labels.text.fontSize}px ${theme.labels.text.fontFamily}` + ctx.textBaseline = 'middle' + ctx.textAlign = 'center' + + barTotals.forEach(barTotal => { + ctx.fillText(String(barTotal.value), barTotal.x, barTotal.y) + }) +} + const InnerBarCanvas = ({ data, indexBy, @@ -366,30 +382,21 @@ const InnerBarCanvas = ({ }) } else if (layer === 'annotations') { renderAnnotationsToCanvas(ctx, { annotations: boundAnnotations, theme }) + } else if (layer === 'totals' && enableTotals) { + const barTotals = computeBarTotals( + bars, + xScale, + yScale, + layout, + groupMode, + totalsOffset + ) + renderTotalsToCanvas(ctx, barTotals, theme) } else if (typeof layer === 'function') { layer(ctx, layerContext) } }) - const barTotals = computeBarTotals( - enableTotals, - bars, - xScale, - yScale, - layout, - groupMode, - totalsOffset - ) - - ctx.fillStyle = theme.text.fill - ctx.font = `bold ${theme.text.fontSize}px sans-serif` - ctx.textBaseline = 'middle' - ctx.textAlign = 'center' - - barTotals.forEach(barTotal => { - ctx.fillText(String(barTotal.value), barTotal.x, barTotal.y) - }) - ctx.save() }, [ axisBottom, diff --git a/packages/bar/src/BarTotals.tsx b/packages/bar/src/BarTotals.tsx new file mode 100644 index 0000000000..6757e7dd2e --- /dev/null +++ b/packages/bar/src/BarTotals.tsx @@ -0,0 +1,29 @@ +import { useTheme } from '@nivo/core' +import { BarTotalsData } from './compute/totals' + +interface Props { + barTotals: BarTotalsData[] +} + +export const BarTotals = ({ barTotals }: Props) => { + const theme = useTheme() + return ( + <> + {barTotals.map(barTotal => ( + + {barTotal.value} + + ))} + + ) +} diff --git a/packages/bar/src/compute/totals.ts b/packages/bar/src/compute/totals.ts index e3e4724ef1..ee8b3bb7e0 100644 --- a/packages/bar/src/compute/totals.ts +++ b/packages/bar/src/compute/totals.ts @@ -2,7 +2,7 @@ import { AnyScale, ScaleBand } from '@nivo/scales' import { defaultProps } from '../props' import { BarCommonProps, BarDatum, ComputedBarDatum } from '../types' -interface BarTotalsData { +export interface BarTotalsData { key: string x: number y: number @@ -10,7 +10,6 @@ interface BarTotalsData { } export const computeBarTotals = ( - enableTotals: boolean, bars: ComputedBarDatum[], xScale: ScaleBand | AnyScale, yScale: ScaleBand | AnyScale, @@ -20,7 +19,7 @@ export const computeBarTotals = ( ) => { const totals = [] as BarTotalsData[] - if (!enableTotals || bars.length === 0) return totals + if (bars.length === 0) return totals const totalsByIndex = new Map() diff --git a/packages/bar/src/index.ts b/packages/bar/src/index.ts index 179dd8da6b..dc66c3babb 100644 --- a/packages/bar/src/index.ts +++ b/packages/bar/src/index.ts @@ -2,6 +2,7 @@ export * from './Bar' 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 08d4c3d6dc..63c59b90b9 100644 --- a/packages/bar/src/props.ts +++ b/packages/bar/src/props.ts @@ -54,7 +54,7 @@ export const defaultProps = { export const svgDefaultProps = { ...defaultProps, - layers: ['grid', 'axes', 'bars', 'markers', 'legends', 'annotations'] as BarLayerId[], + layers: ['grid', 'axes', 'bars', 'markers', 'legends', 'annotations', 'totals'] as BarLayerId[], barComponent: BarItem, defs: [], @@ -69,7 +69,7 @@ export const svgDefaultProps = { export const canvasDefaultProps = { ...defaultProps, - layers: ['grid', 'axes', 'bars', 'legends', 'annotations'] as BarCanvasLayerId[], + layers: ['grid', 'axes', 'bars', 'legends', 'annotations', 'totals'] 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 a225036bf6..135b9f5709 100644 --- a/packages/bar/src/types.ts +++ b/packages/bar/src/types.ts @@ -95,7 +95,7 @@ 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 diff --git a/packages/bar/tests/Bar.test.tsx b/packages/bar/tests/Bar.test.tsx index 38508a8369..e2b33c99eb 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, 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 @@ -631,12 +631,9 @@ describe('totals layer', () => { /> ).root - const totals = instance.findAllByType('text').filter(text => { - return text.props['data-test'] === 'bar-total' - }) - - expect(totals).toHaveLength(3) - totals.forEach((total, index) => { + const totals = instance.findByType(BarTotals) + expect(totals.children).toHaveLength(3) + ;(totals.children as ReactTestInstance[]).forEach((total, index) => { if (index === 0) { expect(total.children[0]).toBe(`2`) } else if (index === 1) { @@ -663,12 +660,9 @@ describe('totals layer', () => { /> ).root - const totals = instance.findAllByType('text').filter(text => { - return text.props['data-test'] === 'bar-total' - }) - - expect(totals).toHaveLength(3) - totals.forEach((total, index) => { + const totals = instance.findByType(BarTotals) + expect(totals.children).toHaveLength(3) + ;(totals.children as ReactTestInstance[]).forEach((total, index) => { if (index === 0) { expect(total.children[0]).toBe(`2`) } else if (index === 1) { @@ -694,12 +688,9 @@ describe('totals layer', () => { /> ).root - const totals = instance.findAllByType('text').filter(text => { - return text.props['data-test'] === 'bar-total' - }) - - expect(totals).toHaveLength(2) - totals.forEach((total, index) => { + const totals = instance.findByType(BarTotals) + expect(totals.children).toHaveLength(2) + ;(totals.children as ReactTestInstance[]).forEach((total, index) => { if (index === 0) { expect(total.children[0]).toBe(`-2`) } else { @@ -725,13 +716,9 @@ describe('totals layer', () => { /> ).root - const totals = instance.findAllByType('text').filter(text => { - return text.props['data-test'] === 'bar-total' - }) - - expect(totals).toHaveLength(3) - - totals.forEach((total, index) => { + const totals = instance.findByType(BarTotals) + expect(totals.children).toHaveLength(3) + ;(totals.children as ReactTestInstance[]).forEach((total, index) => { if (index === 0) { expect(total.children[0]).toBe(`0`) } else if (index === 1) { @@ -751,6 +738,7 @@ describe('totals layer', () => { text: { fontSize: 14, fill: 'red', + fontFamily: 'serif', }, }} keys={['value1', 'value2']} @@ -763,15 +751,13 @@ describe('totals layer', () => { /> ).root - const totals = instance.findAllByType('text').filter(text => { - return text.props['data-test'] === 'bar-total' - }) - - expect(totals).toHaveLength(3) - totals.forEach((total, index) => { + const totals = instance.findByType(BarTotals) + expect(totals.children).toHaveLength(3) + ;(totals.children as ReactTestInstance[]).forEach((total, index) => { const props = total.props expect(props.fill).toBe('red') expect(props.fontSize).toBe(14) + expect(props.fontFamily).toBe('serif') }) }) }) From 4d12cdc16533c8ca8ef7c1c0626055da80e8e51e Mon Sep 17 00:00:00 2001 From: joaopedromatias Date: Tue, 5 Mar 2024 19:49:33 -0300 Subject: [PATCH 12/28] fix: remove unnused variable --- packages/bar/src/Bar.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/bar/src/Bar.tsx b/packages/bar/src/Bar.tsx index 5f57be84a0..e2b2786278 100644 --- a/packages/bar/src/Bar.tsx +++ b/packages/bar/src/Bar.tsx @@ -17,7 +17,6 @@ import { bindDefs, useDimensions, useMotionConfig, - useTheme, } from '@nivo/core' import { Fragment, ReactNode, createElement, useMemo } from 'react' import { svgDefaultProps } from './props' @@ -109,7 +108,6 @@ const InnerBar = ({ enableTotals = svgDefaultProps.enableTotals, totalsOffset = svgDefaultProps.totalsOffset, }: InnerBarProps) => { - const theme = useTheme() const { animate, config: springConfig } = useMotionConfig() const { outerWidth, outerHeight, margin, innerWidth, innerHeight } = useDimensions( width, From e3edd632d33f67673adb7bd5a01e1ce9bfa5d03f Mon Sep 17 00:00:00 2001 From: joaopedromatias Date: Tue, 5 Mar 2024 23:54:29 -0300 Subject: [PATCH 13/28] style: add transitions to bar totals --- packages/bar/src/Bar.tsx | 78 +++++++++++++++++++++++++++------- packages/bar/src/BarTotal.tsx | 32 ++++++++++++++ packages/bar/src/BarTotals.tsx | 29 ------------- packages/bar/src/hooks.ts | 9 ++++ packages/bar/src/index.ts | 4 +- 5 files changed, 105 insertions(+), 47 deletions(-) create mode 100644 packages/bar/src/BarTotal.tsx delete mode 100644 packages/bar/src/BarTotals.tsx diff --git a/packages/bar/src/Bar.tsx b/packages/bar/src/Bar.tsx index e2b2786278..4192cc8261 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,12 +8,22 @@ 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 { BarTotalsData } from './compute/totals' import { useBar } from './hooks' -import { computeBarTotals } from './compute/totals' -import { BarTotals } from './BarTotals' +import { svgDefaultProps } from './props' +import { + BarCustomLayerProps, + BarDatum, + BarLayer, + BarLayerId, + BarSvgProps, + ComputedBarDatumWithValue, +} from './types' +import { BarTotal } from './BarTotal' type InnerBarProps = Omit< BarSvgProps, @@ -127,6 +127,7 @@ const InnerBar = ({ shouldRenderBarLabel, toggleSerie, legendsWithData, + barTotals, } = useBar({ indexBy, label, @@ -156,6 +157,7 @@ const InnerBar = ({ legends, legendLabel, initialHiddenIds, + totalsOffset, }) const transition = useTransition< @@ -281,6 +283,40 @@ const InnerBar = ({ targetKey: 'data.fill', }) + const totalsTransition = useTransition< + BarTotalsData, + { + x: number + y: number + labelOpacity: number + } + >(barTotals, { + keys: barTotal => barTotal.key, + from: barTotal => ({ + x: layout === 'vertical' ? barTotal.x : barTotal.x - 50, + y: layout === 'vertical' ? barTotal.y + 50 : 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.x - 50, + y: layout === 'vertical' ? barTotal.y + 50 : barTotal.y, + labelOpacity: 0, + }), + config: springConfig, + immediate: !animate, + initial: animate ? undefined : null, + }) + const layerById: Record = { annotations: null, axes: null, @@ -369,8 +405,18 @@ const InnerBar = ({ } if (layers.includes('totals') && enableTotals) { - const barTotals = computeBarTotals(bars, xScale, yScale, layout, groupMode, totalsOffset) - layerById.totals = + layerById.totals = ( + + {totalsTransition((style, barTotal) => + createElement(BarTotal, { + value: barTotal.value, + labelOpacity: style.labelOpacity, + x: style.x, + y: style.y, + }) + )} + + ) } const layerContext: BarCustomLayerProps = useMemo( diff --git a/packages/bar/src/BarTotal.tsx b/packages/bar/src/BarTotal.tsx new file mode 100644 index 0000000000..82ed0b116e --- /dev/null +++ b/packages/bar/src/BarTotal.tsx @@ -0,0 +1,32 @@ +import { useTheme } from '@nivo/core' +import { SpringValue, animated } from '@react-spring/web' + +interface Props { + value: number + labelOpacity: SpringValue + x: SpringValue + y: SpringValue +} + +export const BarTotal = ({ value, labelOpacity, x, y }: Props) => { + const theme = useTheme() + return ( + + {value} + + ) +} diff --git a/packages/bar/src/BarTotals.tsx b/packages/bar/src/BarTotals.tsx deleted file mode 100644 index 6757e7dd2e..0000000000 --- a/packages/bar/src/BarTotals.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { useTheme } from '@nivo/core' -import { BarTotalsData } from './compute/totals' - -interface Props { - barTotals: BarTotalsData[] -} - -export const BarTotals = ({ barTotals }: Props) => { - const theme = useTheme() - return ( - <> - {barTotals.map(barTotal => ( - - {barTotal.value} - - ))} - - ) -} diff --git a/packages/bar/src/hooks.ts b/packages/bar/src/hooks.ts index 880b5983b7..39c21d3df7 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), + [bars, xScale, yScale, layout, groupMode, totalsOffset] + ) + 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 dc66c3babb..c28abb3ca7 100644 --- a/packages/bar/src/index.ts +++ b/packages/bar/src/index.ts @@ -1,8 +1,8 @@ export * from './Bar' +export * from './BarCanvas' export * from './BarItem' export * from './BarTooltip' -export * from './BarCanvas' -export * from './BarTotals' +export * from './BarTotal' export * from './ResponsiveBar' export * from './ResponsiveBarCanvas' export * from './props' From 16bd5682c66f93b735c7032f903d16f7dfdcfa07 Mon Sep 17 00:00:00 2001 From: joaopedromatias Date: Wed, 6 Mar 2024 00:08:08 -0300 Subject: [PATCH 14/28] test: update tests to find totals component --- packages/bar/tests/Bar.test.tsx | 70 ++++++++++++++++++--------------- 1 file changed, 39 insertions(+), 31 deletions(-) diff --git a/packages/bar/tests/Bar.test.tsx b/packages/bar/tests/Bar.test.tsx index e2b33c99eb..e2002bbf14 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, type ReactTestInstance } from 'react-test-renderer' import { LegendSvg, LegendSvgItem } from '@nivo/legends' -import { Bar, BarDatum, BarItemProps, ComputedDatum, BarItem, BarTooltip, BarTotals } from '../' +import { Bar, BarDatum, BarItemProps, ComputedDatum, BarItem, BarTooltip, BarTotal } from '../' type IdValue = { id: string @@ -631,15 +631,16 @@ describe('totals layer', () => { /> ).root - const totals = instance.findByType(BarTotals) - expect(totals.children).toHaveLength(3) - ;(totals.children as ReactTestInstance[]).forEach((total, index) => { + const totals = instance.findAllByType(BarTotal) + + totals.forEach((total, index) => { + const value = total.findByType('text').children[0] if (index === 0) { - expect(total.children[0]).toBe(`2`) + expect(value).toBe(`2`) } else if (index === 1) { - expect(total.children[0]).toBe(`3`) + expect(value).toBe(`3`) } else if (index === 2) { - expect(total.children[0]).toBe(`4`) + expect(value).toBe(`4`) } }) }) @@ -660,15 +661,16 @@ describe('totals layer', () => { /> ).root - const totals = instance.findByType(BarTotals) - expect(totals.children).toHaveLength(3) - ;(totals.children as ReactTestInstance[]).forEach((total, index) => { + const totals = instance.findAllByType(BarTotal) + + totals.forEach((total, index) => { + const value = total.findByType('text').children[0] if (index === 0) { - expect(total.children[0]).toBe(`2`) + expect(value).toBe(`2`) } else if (index === 1) { - expect(total.children[0]).toBe(`4`) + expect(value).toBe(`4`) } else if (index === 2) { - expect(total.children[0]).toBe(`6`) + expect(value).toBe(`6`) } }) }) @@ -688,13 +690,14 @@ describe('totals layer', () => { /> ).root - const totals = instance.findByType(BarTotals) - expect(totals.children).toHaveLength(2) - ;(totals.children as ReactTestInstance[]).forEach((total, index) => { + const totals = instance.findAllByType(BarTotal) + + totals.forEach((total, index) => { + const value = total.findByType('text').children[0] if (index === 0) { - expect(total.children[0]).toBe(`-2`) + expect(value).toBe(`-2`) } else { - expect(total.children[0]).toBe(`-4`) + expect(value).toBe(`-4`) } }) }) @@ -716,15 +719,16 @@ describe('totals layer', () => { /> ).root - const totals = instance.findByType(BarTotals) - expect(totals.children).toHaveLength(3) - ;(totals.children as ReactTestInstance[]).forEach((total, index) => { + const totals = instance.findAllByType(BarTotal) + + totals.forEach((total, index) => { + const value = total.findByType('text').children[0] if (index === 0) { - expect(total.children[0]).toBe(`0`) + expect(value).toBe(`0`) } else if (index === 1) { - expect(total.children[0]).toBe(`1`) + expect(value).toBe(`1`) } else if (index === 2) { - expect(total.children[0]).toBe(`3`) + expect(value).toBe(`3`) } }) }) @@ -735,10 +739,14 @@ describe('totals layer', () => { height={300} enableTotals={true} theme={{ + labels: { + text: { + fontSize: 14, + fontFamily: 'serif', + }, + }, text: { - fontSize: 14, fill: 'red', - fontFamily: 'serif', }, }} keys={['value1', 'value2']} @@ -751,11 +759,11 @@ describe('totals layer', () => { /> ).root - const totals = instance.findByType(BarTotals) - expect(totals.children).toHaveLength(3) - ;(totals.children as ReactTestInstance[]).forEach((total, index) => { - const props = total.props - expect(props.fill).toBe('red') + const totals = instance.findAllByType(BarTotal) + + 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') }) From 84e436952761cfbf2a9b4ded93f4a4d5044c5614 Mon Sep 17 00:00:00 2001 From: joaopedromatias Date: Wed, 6 Mar 2024 00:22:52 -0300 Subject: [PATCH 15/28] docs: add enable totals docs on website --- website/src/data/components/bar/props.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/website/src/data/components/bar/props.ts b/website/src/data/components/bar/props.ts index ba5111e69e..8b009e17db 100644 --- a/website/src/data/components/bar/props.ts +++ b/website/src/data/components/bar/props.ts @@ -416,6 +416,16 @@ 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' }, + }, ...chartGrid({ flavors: allFlavors, xDefault: svgDefaultProps.enableGridX, From fcf3194956be462136e4d57f102984c48156d8af Mon Sep 17 00:00:00 2001 From: joaopedromatias Date: Wed, 6 Mar 2024 00:29:08 -0300 Subject: [PATCH 16/28] refactor: use totals computed value through hook on canvas --- packages/bar/src/BarCanvas.tsx | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/packages/bar/src/BarCanvas.tsx b/packages/bar/src/BarCanvas.tsx index 39b8e6ede9..1fdfc4f2b6 100644 --- a/packages/bar/src/BarCanvas.tsx +++ b/packages/bar/src/BarCanvas.tsx @@ -207,6 +207,7 @@ const InnerBarCanvas = ({ getLabelColor, shouldRenderBarLabel, legendsWithData, + barTotals, } = useBar({ indexBy, label, @@ -235,6 +236,7 @@ const InnerBarCanvas = ({ labelSkipHeight, legends, legendLabel, + totalsOffset, }) const { showTooltipFromEvent, hideTooltip } = useTooltip() @@ -383,14 +385,6 @@ const InnerBarCanvas = ({ } else if (layer === 'annotations') { renderAnnotationsToCanvas(ctx, { annotations: boundAnnotations, theme }) } else if (layer === 'totals' && enableTotals) { - const barTotals = computeBarTotals( - bars, - xScale, - yScale, - layout, - groupMode, - totalsOffset - ) renderTotalsToCanvas(ctx, barTotals, theme) } else if (typeof layer === 'function') { layer(ctx, layerContext) @@ -434,9 +428,8 @@ const InnerBarCanvas = ({ shouldRenderBarLabel, theme, width, - bars, + barTotals, enableTotals, - totalsOffset, ]) const handleMouseHover = useCallback( From b582221e85b92b58e7e4e68f9a406220880b841b Mon Sep 17 00:00:00 2001 From: joaopedromatias Date: Wed, 6 Mar 2024 00:35:16 -0300 Subject: [PATCH 17/28] fix: remove unnused var --- packages/bar/src/BarCanvas.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bar/src/BarCanvas.tsx b/packages/bar/src/BarCanvas.tsx index 1fdfc4f2b6..7f4b9777da 100644 --- a/packages/bar/src/BarCanvas.tsx +++ b/packages/bar/src/BarCanvas.tsx @@ -33,7 +33,7 @@ import { renderAxesToCanvas, renderGridLinesToCanvas } from '@nivo/axes' import { renderLegendToCanvas } from '@nivo/legends' import { useTooltip } from '@nivo/tooltip' import { useBar } from './hooks' -import { BarTotalsData, computeBarTotals } from './compute/totals' +import { BarTotalsData } from './compute/totals' type InnerBarCanvasProps = Omit< BarCanvasProps, From 515eb699b2018a22a64c029c9264aab17610450d Mon Sep 17 00:00:00 2001 From: joaopedromatias Date: Wed, 6 Mar 2024 09:56:42 -0300 Subject: [PATCH 18/28] fix: add enableTotals prop to default bar props on website --- website/src/pages/bar/api.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/website/src/pages/bar/api.tsx b/website/src/pages/bar/api.tsx index 5c30e25714..f7ab7bd69d 100644 --- a/website/src/pages/bar/api.tsx +++ b/website/src/pages/bar/api.tsx @@ -109,6 +109,7 @@ const BarApi = () => { enableGridY: true, enableLabel: true, + enableTotals: false, labelSkipWidth: 12, labelSkipHeight: 12, labelTextColor: { From ad8806b47f5b9ea649d51c3efae7b525af769337 Mon Sep 17 00:00:00 2001 From: joaopedromatias Date: Wed, 6 Mar 2024 12:02:27 -0300 Subject: [PATCH 19/28] docs(website): add default enableTotals prop to canvas and svg flavors --- website/src/pages/bar/canvas.js | 1 + website/src/pages/bar/index.js | 1 + 2 files changed, 2 insertions(+) diff --git a/website/src/pages/bar/canvas.js b/website/src/pages/bar/canvas.js index 398521c0d4..19cc27db44 100644 --- a/website/src/pages/bar/canvas.js +++ b/website/src/pages/bar/canvas.js @@ -89,6 +89,7 @@ const initialProperties = { enableGridY: false, enableLabel: true, + enableTotals: false, labelSkipWidth: 12, labelSkipHeight: 12, labelTextColor: { diff --git a/website/src/pages/bar/index.js b/website/src/pages/bar/index.js index 74254db8eb..2ed8d664f0 100644 --- a/website/src/pages/bar/index.js +++ b/website/src/pages/bar/index.js @@ -107,6 +107,7 @@ const initialProperties = { enableGridY: true, enableLabel: true, + enableTotals: false, labelSkipWidth: 12, labelSkipHeight: 12, labelTextColor: { From b3cd571e6751adeac4cea8cdce2ef4e1de28bfd8 Mon Sep 17 00:00:00 2001 From: joaopedromatias Date: Wed, 6 Mar 2024 18:34:28 -0300 Subject: [PATCH 20/28] style: align total label text based on layout mode --- packages/bar/src/Bar.tsx | 1 + packages/bar/src/BarCanvas.tsx | 10 ++++++---- packages/bar/src/BarTotal.tsx | 15 ++++++++++++--- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/packages/bar/src/Bar.tsx b/packages/bar/src/Bar.tsx index 4192cc8261..3ea82712bf 100644 --- a/packages/bar/src/Bar.tsx +++ b/packages/bar/src/Bar.tsx @@ -413,6 +413,7 @@ const InnerBar = ({ labelOpacity: style.labelOpacity, x: style.x, y: style.y, + layout, }) )} diff --git a/packages/bar/src/BarCanvas.tsx b/packages/bar/src/BarCanvas.tsx index 7f4b9777da..f15957fcdf 100644 --- a/packages/bar/src/BarCanvas.tsx +++ b/packages/bar/src/BarCanvas.tsx @@ -2,6 +2,7 @@ import { BarCanvasCustomLayerProps, BarCanvasLayer, BarCanvasProps, + BarCommonProps, BarDatum, ComputedBarDatum, } from './types' @@ -54,15 +55,16 @@ const findBarUnderCursor = ( const isNumber = (value: unknown): value is number => typeof value === 'number' -function renderTotalsToCanvas( +function renderTotalsToCanvas( ctx: CanvasRenderingContext2D, barTotals: BarTotalsData[], - theme: CompleteTheme + 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 = 'middle' - ctx.textAlign = 'center' + ctx.textAlign = layout === 'vertical' ? 'center' : 'start' barTotals.forEach(barTotal => { ctx.fillText(String(barTotal.value), barTotal.x, barTotal.y) @@ -385,7 +387,7 @@ const InnerBarCanvas = ({ } else if (layer === 'annotations') { renderAnnotationsToCanvas(ctx, { annotations: boundAnnotations, theme }) } else if (layer === 'totals' && enableTotals) { - renderTotalsToCanvas(ctx, barTotals, theme) + renderTotalsToCanvas(ctx, barTotals, theme, layout) } else if (typeof layer === 'function') { layer(ctx, layerContext) } diff --git a/packages/bar/src/BarTotal.tsx b/packages/bar/src/BarTotal.tsx index 82ed0b116e..7f71d0897b 100644 --- a/packages/bar/src/BarTotal.tsx +++ b/packages/bar/src/BarTotal.tsx @@ -1,14 +1,23 @@ import { useTheme } from '@nivo/core' import { SpringValue, animated } from '@react-spring/web' +import { BarCommonProps, BarDatum } from './types' +import { svgDefaultProps } from './props' -interface Props { +interface Props { value: number labelOpacity: SpringValue x: SpringValue y: SpringValue + layout?: BarCommonProps['layout'] } -export const BarTotal = ({ value, labelOpacity, x, y }: Props) => { +export const BarTotal = ({ + value, + labelOpacity, + x, + y, + layout = svgDefaultProps.layout, +}: Props) => { const theme = useTheme() return ( { fontWeight="bold" fontSize={theme.labels.text.fontSize} fontFamily={theme.labels.text.fontFamily} - textAnchor="middle" + textAnchor={layout === 'vertical' ? 'middle' : 'start'} alignmentBaseline="middle" > {value} From 49e30e048bdbec0c474baa485631e909a1b852be Mon Sep 17 00:00:00 2001 From: joaopedromatias Date: Thu, 7 Mar 2024 09:35:46 -0300 Subject: [PATCH 21/28] feat: add value format to totals labels --- packages/bar/src/Bar.tsx | 1 + packages/bar/src/BarCanvas.tsx | 9 +++++++-- packages/bar/src/BarTotal.tsx | 8 ++++++-- packages/bar/tests/Bar.test.tsx | 7 ++++--- 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/packages/bar/src/Bar.tsx b/packages/bar/src/Bar.tsx index 3ea82712bf..74ae45a596 100644 --- a/packages/bar/src/Bar.tsx +++ b/packages/bar/src/Bar.tsx @@ -413,6 +413,7 @@ const InnerBar = ({ labelOpacity: style.labelOpacity, x: style.x, y: style.y, + valueFormat, layout, }) )} diff --git a/packages/bar/src/BarCanvas.tsx b/packages/bar/src/BarCanvas.tsx index f15957fcdf..903b36fe25 100644 --- a/packages/bar/src/BarCanvas.tsx +++ b/packages/bar/src/BarCanvas.tsx @@ -14,6 +14,7 @@ import { isCursorInRect, useDimensions, useTheme, + useValueFormatter, } from '@nivo/core' import { ForwardedRef, @@ -59,6 +60,7 @@ function renderTotalsToCanvas( ctx: CanvasRenderingContext2D, barTotals: BarTotalsData[], theme: CompleteTheme, + formatValue: (value: number) => string, layout: BarCommonProps['layout'] = canvasDefaultProps.layout ) { ctx.fillStyle = theme.text.fill @@ -67,7 +69,7 @@ function renderTotalsToCanvas( ctx.textAlign = layout === 'vertical' ? 'center' : 'start' barTotals.forEach(barTotal => { - ctx.fillText(String(barTotal.value), barTotal.x, barTotal.y) + ctx.fillText(formatValue(barTotal.value), barTotal.x, barTotal.y) }) } @@ -309,6 +311,8 @@ const InnerBarCanvas = ({ ] ) + const formatValue = useValueFormatter(valueFormat) + useEffect(() => { const ctx = canvasEl.current?.getContext('2d') @@ -387,7 +391,7 @@ const InnerBarCanvas = ({ } else if (layer === 'annotations') { renderAnnotationsToCanvas(ctx, { annotations: boundAnnotations, theme }) } else if (layer === 'totals' && enableTotals) { - renderTotalsToCanvas(ctx, barTotals, theme, layout) + renderTotalsToCanvas(ctx, barTotals, theme, formatValue, layout) } else if (typeof layer === 'function') { layer(ctx, layerContext) } @@ -432,6 +436,7 @@ const InnerBarCanvas = ({ width, barTotals, enableTotals, + formatValue, ]) const handleMouseHover = useCallback( diff --git a/packages/bar/src/BarTotal.tsx b/packages/bar/src/BarTotal.tsx index 7f71d0897b..dcb2f1fc31 100644 --- a/packages/bar/src/BarTotal.tsx +++ b/packages/bar/src/BarTotal.tsx @@ -1,4 +1,4 @@ -import { useTheme } from '@nivo/core' +import { useTheme, useValueFormatter } from '@nivo/core' import { SpringValue, animated } from '@react-spring/web' import { BarCommonProps, BarDatum } from './types' import { svgDefaultProps } from './props' @@ -8,6 +8,7 @@ interface Props { labelOpacity: SpringValue x: SpringValue y: SpringValue + valueFormat: BarCommonProps['valueFormat'] layout?: BarCommonProps['layout'] } @@ -16,9 +17,12 @@ export const BarTotal = ({ labelOpacity, x, y, + valueFormat, layout = svgDefaultProps.layout, }: Props) => { const theme = useTheme() + const formatValue = useValueFormatter(valueFormat) + return ( ({ textAnchor={layout === 'vertical' ? 'middle' : 'start'} alignmentBaseline="middle" > - {value} + {formatValue(value)} ) } diff --git a/packages/bar/tests/Bar.test.tsx b/packages/bar/tests/Bar.test.tsx index e2002bbf14..9789afb7d0 100644 --- a/packages/bar/tests/Bar.test.tsx +++ b/packages/bar/tests/Bar.test.tsx @@ -658,6 +658,7 @@ describe('totals layer', () => { { id: 'three', value1: 3, value2: 3 }, ]} animate={false} + valueFormat=" >-$" /> ).root @@ -666,11 +667,11 @@ describe('totals layer', () => { totals.forEach((total, index) => { const value = total.findByType('text').children[0] if (index === 0) { - expect(value).toBe(`2`) + expect(value).toBe(`$2`) } else if (index === 1) { - expect(value).toBe(`4`) + expect(value).toBe(`$4`) } else if (index === 2) { - expect(value).toBe(`6`) + expect(value).toBe(`$6`) } }) }) From 81b1756081ca77d7944a7aa10a1900b4ae439627 Mon Sep 17 00:00:00 2001 From: joaopedromatias Date: Thu, 7 Mar 2024 23:01:23 -0300 Subject: [PATCH 22/28] refactor: configure totals transition inside its component --- packages/bar/src/Bar.tsx | 55 ++++--------------------- packages/bar/src/BarTotal.tsx | 45 -------------------- packages/bar/src/BarTotals.tsx | 75 ++++++++++++++++++++++++++++++++++ packages/bar/src/index.ts | 2 +- 4 files changed, 83 insertions(+), 94 deletions(-) delete mode 100644 packages/bar/src/BarTotal.tsx create mode 100644 packages/bar/src/BarTotals.tsx diff --git a/packages/bar/src/Bar.tsx b/packages/bar/src/Bar.tsx index 74ae45a596..d0dd26ec15 100644 --- a/packages/bar/src/Bar.tsx +++ b/packages/bar/src/Bar.tsx @@ -12,7 +12,6 @@ import { useTransition } from '@react-spring/web' import { Fragment, ReactNode, createElement, useMemo } from 'react' import { BarAnnotations } from './BarAnnotations' import { BarLegends } from './BarLegends' -import { BarTotalsData } from './compute/totals' import { useBar } from './hooks' import { svgDefaultProps } from './props' import { @@ -23,7 +22,7 @@ import { BarSvgProps, ComputedBarDatumWithValue, } from './types' -import { BarTotal } from './BarTotal' +import { BarTotals } from './BarTotals' type InnerBarProps = Omit< BarSvgProps, @@ -283,40 +282,6 @@ const InnerBar = ({ targetKey: 'data.fill', }) - const totalsTransition = useTransition< - BarTotalsData, - { - x: number - y: number - labelOpacity: number - } - >(barTotals, { - keys: barTotal => barTotal.key, - from: barTotal => ({ - x: layout === 'vertical' ? barTotal.x : barTotal.x - 50, - y: layout === 'vertical' ? barTotal.y + 50 : 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.x - 50, - y: layout === 'vertical' ? barTotal.y + 50 : barTotal.y, - labelOpacity: 0, - }), - config: springConfig, - immediate: !animate, - initial: animate ? undefined : null, - }) - const layerById: Record = { annotations: null, axes: null, @@ -406,18 +371,12 @@ const InnerBar = ({ if (layers.includes('totals') && enableTotals) { layerById.totals = ( - - {totalsTransition((style, barTotal) => - createElement(BarTotal, { - value: barTotal.value, - labelOpacity: style.labelOpacity, - x: style.x, - y: style.y, - valueFormat, - layout, - }) - )} - + ) } diff --git a/packages/bar/src/BarTotal.tsx b/packages/bar/src/BarTotal.tsx deleted file mode 100644 index dcb2f1fc31..0000000000 --- a/packages/bar/src/BarTotal.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { useTheme, useValueFormatter } from '@nivo/core' -import { SpringValue, animated } from '@react-spring/web' -import { BarCommonProps, BarDatum } from './types' -import { svgDefaultProps } from './props' - -interface Props { - value: number - labelOpacity: SpringValue - x: SpringValue - y: SpringValue - valueFormat: BarCommonProps['valueFormat'] - layout?: BarCommonProps['layout'] -} - -export const BarTotal = ({ - value, - labelOpacity, - x, - y, - valueFormat, - layout = svgDefaultProps.layout, -}: Props) => { - const theme = useTheme() - const formatValue = useValueFormatter(valueFormat) - - return ( - - {formatValue(value)} - - ) -} diff --git a/packages/bar/src/BarTotals.tsx b/packages/bar/src/BarTotals.tsx new file mode 100644 index 0000000000..ccb62f2011 --- /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.x - 50, + y: layout === 'vertical' ? barTotal.y + 50 : 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.x - 50, + y: layout === 'vertical' ? barTotal.y + 50 : barTotal.y, + labelOpacity: 0, + }), + config: springConfig, + immediate: !animate, + initial: animate ? undefined : null, + }) + + return totalsTransition((style, barTotal) => ( + + {barTotal.value} + + )) +} diff --git a/packages/bar/src/index.ts b/packages/bar/src/index.ts index c28abb3ca7..45855e52bd 100644 --- a/packages/bar/src/index.ts +++ b/packages/bar/src/index.ts @@ -2,7 +2,7 @@ export * from './Bar' export * from './BarCanvas' export * from './BarItem' export * from './BarTooltip' -export * from './BarTotal' +export * from './BarTotals' export * from './ResponsiveBar' export * from './ResponsiveBarCanvas' export * from './props' From 66eb0f126c24e07c82bfbf2f0a583bc001a8969a Mon Sep 17 00:00:00 2001 From: joaopedromatias Date: Thu, 7 Mar 2024 23:16:00 -0300 Subject: [PATCH 23/28] refactor: format value in totals compute function --- packages/bar/src/BarCanvas.tsx | 5 ++--- packages/bar/src/compute/totals.ts | 9 +++++---- packages/bar/src/hooks.ts | 4 ++-- packages/bar/tests/Bar.test.tsx | 12 ++++++------ 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/bar/src/BarCanvas.tsx b/packages/bar/src/BarCanvas.tsx index 903b36fe25..5f44a65cf7 100644 --- a/packages/bar/src/BarCanvas.tsx +++ b/packages/bar/src/BarCanvas.tsx @@ -60,7 +60,6 @@ function renderTotalsToCanvas( ctx: CanvasRenderingContext2D, barTotals: BarTotalsData[], theme: CompleteTheme, - formatValue: (value: number) => string, layout: BarCommonProps['layout'] = canvasDefaultProps.layout ) { ctx.fillStyle = theme.text.fill @@ -69,7 +68,7 @@ function renderTotalsToCanvas( ctx.textAlign = layout === 'vertical' ? 'center' : 'start' barTotals.forEach(barTotal => { - ctx.fillText(formatValue(barTotal.value), barTotal.x, barTotal.y) + ctx.fillText(barTotal.value, barTotal.x, barTotal.y) }) } @@ -391,7 +390,7 @@ const InnerBarCanvas = ({ } else if (layer === 'annotations') { renderAnnotationsToCanvas(ctx, { annotations: boundAnnotations, theme }) } else if (layer === 'totals' && enableTotals) { - renderTotalsToCanvas(ctx, barTotals, theme, formatValue, layout) + renderTotalsToCanvas(ctx, barTotals, theme, layout) } else if (typeof layer === 'function') { layer(ctx, layerContext) } diff --git a/packages/bar/src/compute/totals.ts b/packages/bar/src/compute/totals.ts index ee8b3bb7e0..e55a1bc162 100644 --- a/packages/bar/src/compute/totals.ts +++ b/packages/bar/src/compute/totals.ts @@ -6,7 +6,7 @@ export interface BarTotalsData { key: string x: number y: number - value: number + value: string } export const computeBarTotals = ( @@ -15,7 +15,8 @@ export const computeBarTotals = ( yScale: ScaleBand | AnyScale, layout: BarCommonProps['layout'] = defaultProps.layout, groupMode: BarCommonProps['groupMode'] = defaultProps.groupMode, - totalsOffset: number + totalsOffset: number, + formatValue: (value: number) => string ) => { const totals = [] as BarTotalsData[] @@ -56,7 +57,7 @@ export const computeBarTotals = ( key: 'total_' + indexValue, x: xPosition, y: yPosition, - value: indexTotal, + value: formatValue(indexTotal), }) }) } else if (groupMode === 'grouped') { @@ -94,7 +95,7 @@ export const computeBarTotals = ( key: 'total_' + indexValue, x: xPosition, y: yPosition, - value: indexTotal, + value: formatValue(indexTotal), }) }) } diff --git a/packages/bar/src/hooks.ts b/packages/bar/src/hooks.ts index 39c21d3df7..635b711c4c 100644 --- a/packages/bar/src/hooks.ts +++ b/packages/bar/src/hooks.ts @@ -171,8 +171,8 @@ export const useBar = ({ ) const barTotals = useMemo( - () => computeBarTotals(bars, xScale, yScale, layout, groupMode, totalsOffset), - [bars, xScale, yScale, layout, groupMode, totalsOffset] + () => computeBarTotals(bars, xScale, yScale, layout, groupMode, totalsOffset, formatValue), + [bars, xScale, yScale, layout, groupMode, totalsOffset, formatValue] ) return { diff --git a/packages/bar/tests/Bar.test.tsx b/packages/bar/tests/Bar.test.tsx index 9789afb7d0..1fe68574c7 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, type ReactTestInstance } from 'react-test-renderer' import { LegendSvg, LegendSvgItem } from '@nivo/legends' -import { Bar, BarDatum, BarItemProps, ComputedDatum, BarItem, BarTooltip, BarTotal } from '../' +import { Bar, BarDatum, BarItemProps, ComputedDatum, BarItem, BarTooltip, BarTotals } from '../' type IdValue = { id: string @@ -631,7 +631,7 @@ describe('totals layer', () => { /> ).root - const totals = instance.findAllByType(BarTotal) + const totals = instance.findByType(BarTotals).findAllByType('text') totals.forEach((total, index) => { const value = total.findByType('text').children[0] @@ -662,7 +662,7 @@ describe('totals layer', () => { /> ).root - const totals = instance.findAllByType(BarTotal) + const totals = instance.findByType(BarTotals).findAllByType('text') totals.forEach((total, index) => { const value = total.findByType('text').children[0] @@ -691,7 +691,7 @@ describe('totals layer', () => { /> ).root - const totals = instance.findAllByType(BarTotal) + const totals = instance.findByType(BarTotals).findAllByType('text') totals.forEach((total, index) => { const value = total.findByType('text').children[0] @@ -720,7 +720,7 @@ describe('totals layer', () => { /> ).root - const totals = instance.findAllByType(BarTotal) + const totals = instance.findByType(BarTotals).findAllByType('text') totals.forEach((total, index) => { const value = total.findByType('text').children[0] @@ -760,7 +760,7 @@ describe('totals layer', () => { /> ).root - const totals = instance.findAllByType(BarTotal) + const totals = instance.findByType(BarTotals).findAllByType('text') totals.forEach((total, index) => { const props = total.findByType('text').props From 30503507c1d7c6998a03ce02c10c4dcf660aa2fc Mon Sep 17 00:00:00 2001 From: joaopedromatias Date: Thu, 7 Mar 2024 23:36:40 -0300 Subject: [PATCH 24/28] style: prevent overlap on zero totals offset --- packages/bar/src/BarCanvas.tsx | 2 +- packages/bar/src/BarTotals.tsx | 2 +- website/src/data/components/bar/props.ts | 17 ++++++++++++++++- website/src/pages/bar/api.tsx | 1 + website/src/pages/bar/canvas.js | 1 + website/src/pages/bar/index.js | 1 + 6 files changed, 21 insertions(+), 3 deletions(-) diff --git a/packages/bar/src/BarCanvas.tsx b/packages/bar/src/BarCanvas.tsx index 5f44a65cf7..81a05e5c3c 100644 --- a/packages/bar/src/BarCanvas.tsx +++ b/packages/bar/src/BarCanvas.tsx @@ -64,7 +64,7 @@ function renderTotalsToCanvas( ) { ctx.fillStyle = theme.text.fill ctx.font = `bold ${theme.labels.text.fontSize}px ${theme.labels.text.fontFamily}` - ctx.textBaseline = 'middle' + ctx.textBaseline = layout === 'vertical' ? 'alphabetic' : 'middle' ctx.textAlign = layout === 'vertical' ? 'center' : 'start' barTotals.forEach(barTotal => { diff --git a/packages/bar/src/BarTotals.tsx b/packages/bar/src/BarTotals.tsx index ccb62f2011..708c841fec 100644 --- a/packages/bar/src/BarTotals.tsx +++ b/packages/bar/src/BarTotals.tsx @@ -67,7 +67,7 @@ export const BarTotals = ({ fontSize={theme.labels.text.fontSize} fontFamily={theme.labels.text.fontFamily} textAnchor={layout === 'vertical' ? 'middle' : 'start'} - alignmentBaseline="middle" + alignmentBaseline={layout === 'vertical' ? 'alphabetic' : 'middle'} > {barTotal.value} diff --git a/website/src/data/components/bar/props.ts b/website/src/data/components/bar/props.ts index 8b009e17db..2e5cd406b4 100644 --- a/website/src/data/components/bar/props.ts +++ b/website/src/data/components/bar/props.ts @@ -418,7 +418,7 @@ const props: ChartProperty[] = [ }, { key: 'enableTotals', - help: 'Enable/disable totals labels', + help: 'Enable/disable totals labels.', type: 'boolean', flavors: ['svg', 'canvas', 'api'], required: false, @@ -426,6 +426,21 @@ const props: ChartProperty[] = [ 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 f7ab7bd69d..198fe61915 100644 --- a/website/src/pages/bar/api.tsx +++ b/website/src/pages/bar/api.tsx @@ -110,6 +110,7 @@ const BarApi = () => { 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 19cc27db44..f053bd1d53 100644 --- a/website/src/pages/bar/canvas.js +++ b/website/src/pages/bar/canvas.js @@ -90,6 +90,7 @@ const initialProperties = { 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 2ed8d664f0..e7c91f2d13 100644 --- a/website/src/pages/bar/index.js +++ b/website/src/pages/bar/index.js @@ -108,6 +108,7 @@ const initialProperties = { enableLabel: true, enableTotals: false, + totalsOffset: 10, labelSkipWidth: 12, labelSkipHeight: 12, labelTextColor: { From f3c0876af45a7139657f8e1bd776a460b436d0b6 Mon Sep 17 00:00:00 2001 From: joaopedromatias Date: Thu, 7 Mar 2024 23:39:28 -0300 Subject: [PATCH 25/28] types: remove optional syntax --- packages/bar/src/types.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/bar/src/types.ts b/packages/bar/src/types.ts index 135b9f5709..ec545851ed 100644 --- a/packages/bar/src/types.ts +++ b/packages/bar/src/types.ts @@ -259,8 +259,8 @@ export type BarCommonProps = { initialHiddenIds: readonly (string | number)[] - enableTotals?: boolean - totalsOffset?: number + enableTotals: boolean + totalsOffset: number } export type BarSvgProps = Partial> & From f6219503941fbf923d4b8a5fc80bb46140bd7dd1 Mon Sep 17 00:00:00 2001 From: joaopedromatias Date: Fri, 8 Mar 2024 00:31:03 -0300 Subject: [PATCH 26/28] feat: animation offset is calculated individually by index --- packages/bar/src/BarTotals.tsx | 8 ++++---- packages/bar/src/compute/totals.ts | 14 ++++++++++++-- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/packages/bar/src/BarTotals.tsx b/packages/bar/src/BarTotals.tsx index 708c841fec..74465fb3ca 100644 --- a/packages/bar/src/BarTotals.tsx +++ b/packages/bar/src/BarTotals.tsx @@ -28,8 +28,8 @@ export const BarTotals = ({ >(data, { keys: barTotal => barTotal.key, from: barTotal => ({ - x: layout === 'vertical' ? barTotal.x : barTotal.x - 50, - y: layout === 'vertical' ? barTotal.y + 50 : barTotal.y, + x: layout === 'vertical' ? barTotal.x : barTotal.animationOffset, + y: layout === 'vertical' ? barTotal.animationOffset : barTotal.y, labelOpacity: 0, }), enter: barTotal => ({ @@ -43,8 +43,8 @@ export const BarTotals = ({ labelOpacity: 1, }), leave: barTotal => ({ - x: layout === 'vertical' ? barTotal.x : barTotal.x - 50, - y: layout === 'vertical' ? barTotal.y + 50 : barTotal.y, + x: layout === 'vertical' ? barTotal.x : barTotal.animationOffset, + y: layout === 'vertical' ? barTotal.animationOffset : barTotal.y, labelOpacity: 0, }), config: springConfig, diff --git a/packages/bar/src/compute/totals.ts b/packages/bar/src/compute/totals.ts index e55a1bc162..ec689ae886 100644 --- a/packages/bar/src/compute/totals.ts +++ b/packages/bar/src/compute/totals.ts @@ -7,6 +7,7 @@ export interface BarTotalsData { x: number y: number value: string + animationOffset: number } export const computeBarTotals = ( @@ -41,13 +42,16 @@ export const computeBarTotals = ( 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 @@ -58,6 +62,7 @@ export const computeBarTotals = ( x: xPosition, y: yPosition, value: formatValue(indexTotal), + animationOffset, }) }) } else if (groupMode === 'grouped') { @@ -73,20 +78,24 @@ export const computeBarTotals = ( 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 = numberOfBarsByIndex.get(indexValue) * barWidth - const indexBarsHeight = numberOfBarsByIndex.get(indexValue) * barHeight + const indexBarsWidth = numberOfBars * barWidth + const indexBarsHeight = numberOfBars * barHeight xPosition += layout === 'vertical' ? indexBarsWidth / 2 : totalsOffset yPosition += layout === 'vertical' ? -totalsOffset : indexBarsHeight / 2 @@ -96,6 +105,7 @@ export const computeBarTotals = ( x: xPosition, y: yPosition, value: formatValue(indexTotal), + animationOffset, }) }) } From ab67405de5a6bc11bf09347f8d0df8103e990391 Mon Sep 17 00:00:00 2001 From: joaopedromatias Date: Wed, 13 Mar 2024 07:57:30 -0300 Subject: [PATCH 27/28] chore: change order of initializing default layers --- packages/bar/src/props.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/bar/src/props.ts b/packages/bar/src/props.ts index 63c59b90b9..98c53fcf72 100644 --- a/packages/bar/src/props.ts +++ b/packages/bar/src/props.ts @@ -54,7 +54,7 @@ export const defaultProps = { export const svgDefaultProps = { ...defaultProps, - layers: ['grid', 'axes', 'bars', 'markers', 'legends', 'annotations', 'totals'] as BarLayerId[], + layers: ['grid', 'axes', 'bars', 'totals', 'markers', 'legends', 'annotations'] as BarLayerId[], barComponent: BarItem, defs: [], @@ -69,7 +69,7 @@ export const svgDefaultProps = { export const canvasDefaultProps = { ...defaultProps, - layers: ['grid', 'axes', 'bars', 'legends', 'annotations', 'totals'] as BarCanvasLayerId[], + layers: ['grid', 'axes', 'bars', 'totals', 'legends', 'annotations'] as BarCanvasLayerId[], pixelRatio: typeof window !== 'undefined' ? window.devicePixelRatio ?? 1 : 1, } From 999ba05cc66407fde868072c0c1087a7b3161dec Mon Sep 17 00:00:00 2001 From: joaopedromatias Date: Mon, 18 Mar 2024 22:46:34 -0300 Subject: [PATCH 28/28] refactor: add numeric value to bar totals data --- packages/bar/src/BarCanvas.tsx | 2 +- packages/bar/src/BarTotals.tsx | 2 +- packages/bar/src/compute/totals.ts | 9 ++++++--- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/bar/src/BarCanvas.tsx b/packages/bar/src/BarCanvas.tsx index 81a05e5c3c..fefd2a04f2 100644 --- a/packages/bar/src/BarCanvas.tsx +++ b/packages/bar/src/BarCanvas.tsx @@ -68,7 +68,7 @@ function renderTotalsToCanvas( ctx.textAlign = layout === 'vertical' ? 'center' : 'start' barTotals.forEach(barTotal => { - ctx.fillText(barTotal.value, barTotal.x, barTotal.y) + ctx.fillText(barTotal.formattedValue, barTotal.x, barTotal.y) }) } diff --git a/packages/bar/src/BarTotals.tsx b/packages/bar/src/BarTotals.tsx index 74465fb3ca..dc6c0ea5cb 100644 --- a/packages/bar/src/BarTotals.tsx +++ b/packages/bar/src/BarTotals.tsx @@ -69,7 +69,7 @@ export const BarTotals = ({ textAnchor={layout === 'vertical' ? 'middle' : 'start'} alignmentBaseline={layout === 'vertical' ? 'alphabetic' : 'middle'} > - {barTotal.value} + {barTotal.formattedValue} )) } diff --git a/packages/bar/src/compute/totals.ts b/packages/bar/src/compute/totals.ts index ec689ae886..f9b29ceaab 100644 --- a/packages/bar/src/compute/totals.ts +++ b/packages/bar/src/compute/totals.ts @@ -6,7 +6,8 @@ export interface BarTotalsData { key: string x: number y: number - value: string + value: number + formattedValue: string animationOffset: number } @@ -61,7 +62,8 @@ export const computeBarTotals = ( key: 'total_' + indexValue, x: xPosition, y: yPosition, - value: formatValue(indexTotal), + value: indexTotal, + formattedValue: formatValue(indexTotal), animationOffset, }) }) @@ -104,7 +106,8 @@ export const computeBarTotals = ( key: 'total_' + indexValue, x: xPosition, y: yPosition, - value: formatValue(indexTotal), + value: indexTotal, + formattedValue: formatValue(indexTotal), animationOffset, }) })