Skip to content

Commit

Permalink
feat(bar): add bar totals (#2525)
Browse files Browse the repository at this point in the history
* feat: add bar totals layer

* fix: insert unique key prop to each total

* test: insert tests to totals bar layer

* docs: insert recipe on website bar page

* fix: update scale functions types

* refactor: enable totals by prop instead of directly on layers

* style: apply theme configuration on totals

* feat: make totals offsets configurable

* refactor: centralize compute of totals bar

* chore: re-format website docs yml

* refactor: use props along with layers to enable totals

* fix: remove unnused variable

* style: add transitions to bar totals

* test: update tests to find totals component

* docs: add enable totals docs on website

* refactor: use totals computed value through hook on canvas

* fix: remove unnused var

* fix: add enableTotals prop to default bar props on website

* docs(website): add default enableTotals prop to canvas and svg flavors

* style: align total label text based on layout mode

* feat: add value format to totals labels

* refactor: configure totals transition inside its component

* refactor: format value in totals compute function

* style: prevent overlap on zero totals offset

* types: remove optional syntax

* feat: animation offset is calculated individually by index

* chore: change order of initializing default layers

* refactor: add numeric value to bar totals data
  • Loading branch information
joaopedromatias committed Mar 19, 2024
1 parent 154056b commit d90a326
Show file tree
Hide file tree
Showing 15 changed files with 512 additions and 29 deletions.
42 changes: 30 additions & 12 deletions 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,
Expand All @@ -18,10 +8,21 @@ import {
useDimensions,
useMotionConfig,
} from '@nivo/core'
import { Fragment, ReactNode, createElement, useMemo } from 'react'
import { svgDefaultProps } from './props'
import { useTransition } from '@react-spring/web'
import { Fragment, ReactNode, createElement, useMemo } from 'react'
import { BarAnnotations } from './BarAnnotations'
import { BarLegends } from './BarLegends'
import { useBar } from './hooks'
import { svgDefaultProps } from './props'
import {
BarCustomLayerProps,
BarDatum,
BarLayer,
BarLayerId,
BarSvgProps,
ComputedBarDatumWithValue,
} from './types'
import { BarTotals } from './BarTotals'

type InnerBarProps<RawDatum extends BarDatum> = Omit<
BarSvgProps<RawDatum>,
Expand Down Expand Up @@ -102,6 +103,9 @@ const InnerBar = <RawDatum extends BarDatum>({
barAriaDescribedBy,

initialHiddenIds,

enableTotals = svgDefaultProps.enableTotals,
totalsOffset = svgDefaultProps.totalsOffset,
}: InnerBarProps<RawDatum>) => {
const { animate, config: springConfig } = useMotionConfig()
const { outerWidth, outerHeight, margin, innerWidth, innerHeight } = useDimensions(
Expand All @@ -122,6 +126,7 @@ const InnerBar = <RawDatum extends BarDatum>({
shouldRenderBarLabel,
toggleSerie,
legendsWithData,
barTotals,
} = useBar<RawDatum>({
indexBy,
label,
Expand Down Expand Up @@ -151,6 +156,7 @@ const InnerBar = <RawDatum extends BarDatum>({
legends,
legendLabel,
initialHiddenIds,
totalsOffset,
})

const transition = useTransition<
Expand Down Expand Up @@ -283,6 +289,7 @@ const InnerBar = <RawDatum extends BarDatum>({
grid: null,
legends: null,
markers: null,
totals: null,
}

if (layers.includes('annotations')) {
Expand Down Expand Up @@ -362,6 +369,17 @@ const InnerBar = <RawDatum extends BarDatum>({
)
}

if (layers.includes('totals') && enableTotals) {
layerById.totals = (
<BarTotals
data={barTotals}
springConfig={springConfig}
animate={animate}
layout={layout}
/>
)
}

const layerContext: BarCustomLayerProps<RawDatum> = useMemo(
() => ({
...commonProps,
Expand Down
32 changes: 32 additions & 0 deletions packages/bar/src/BarCanvas.tsx
Expand Up @@ -2,16 +2,19 @@ import {
BarCanvasCustomLayerProps,
BarCanvasLayer,
BarCanvasProps,
BarCommonProps,
BarDatum,
ComputedBarDatum,
} from './types'
import {
CompleteTheme,
Container,
Margin,
getRelativeCursor,
isCursorInRect,
useDimensions,
useTheme,
useValueFormatter,
} from '@nivo/core'
import {
ForwardedRef,
Expand All @@ -32,6 +35,7 @@ import { renderAxesToCanvas, renderGridLinesToCanvas } from '@nivo/axes'
import { renderLegendToCanvas } from '@nivo/legends'
import { useTooltip } from '@nivo/tooltip'
import { useBar } from './hooks'
import { BarTotalsData } from './compute/totals'

type InnerBarCanvasProps<RawDatum extends BarDatum> = Omit<
BarCanvasProps<RawDatum>,
Expand All @@ -52,6 +56,22 @@ const findBarUnderCursor = <RawDatum,>(

const isNumber = (value: unknown): value is number => typeof value === 'number'

function renderTotalsToCanvas<RawDatum extends BarDatum>(
ctx: CanvasRenderingContext2D,
barTotals: BarTotalsData[],
theme: CompleteTheme,
layout: BarCommonProps<RawDatum>['layout'] = canvasDefaultProps.layout
) {
ctx.fillStyle = theme.text.fill
ctx.font = `bold ${theme.labels.text.fontSize}px ${theme.labels.text.fontFamily}`
ctx.textBaseline = layout === 'vertical' ? 'alphabetic' : 'middle'
ctx.textAlign = layout === 'vertical' ? 'center' : 'start'

barTotals.forEach(barTotal => {
ctx.fillText(barTotal.formattedValue, barTotal.x, barTotal.y)
})
}

const InnerBarCanvas = <RawDatum extends BarDatum>({
data,
indexBy,
Expand Down Expand Up @@ -166,6 +186,9 @@ const InnerBarCanvas = <RawDatum extends BarDatum>({
pixelRatio = canvasDefaultProps.pixelRatio,

canvasRef,

enableTotals = canvasDefaultProps.enableTotals,
totalsOffset = canvasDefaultProps.totalsOffset,
}: InnerBarCanvasProps<RawDatum>) => {
const canvasEl = useRef<HTMLCanvasElement | null>(null)

Expand All @@ -187,6 +210,7 @@ const InnerBarCanvas = <RawDatum extends BarDatum>({
getLabelColor,
shouldRenderBarLabel,
legendsWithData,
barTotals,
} = useBar<RawDatum>({
indexBy,
label,
Expand Down Expand Up @@ -215,6 +239,7 @@ const InnerBarCanvas = <RawDatum extends BarDatum>({
labelSkipHeight,
legends,
legendLabel,
totalsOffset,
})

const { showTooltipFromEvent, hideTooltip } = useTooltip()
Expand Down Expand Up @@ -285,6 +310,8 @@ const InnerBarCanvas = <RawDatum extends BarDatum>({
]
)

const formatValue = useValueFormatter(valueFormat)

useEffect(() => {
const ctx = canvasEl.current?.getContext('2d')

Expand Down Expand Up @@ -362,6 +389,8 @@ const InnerBarCanvas = <RawDatum extends BarDatum>({
})
} else if (layer === 'annotations') {
renderAnnotationsToCanvas(ctx, { annotations: boundAnnotations, theme })
} else if (layer === 'totals' && enableTotals) {
renderTotalsToCanvas(ctx, barTotals, theme, layout)
} else if (typeof layer === 'function') {
layer(ctx, layerContext)
}
Expand Down Expand Up @@ -404,6 +433,9 @@ const InnerBarCanvas = <RawDatum extends BarDatum>({
shouldRenderBarLabel,
theme,
width,
barTotals,
enableTotals,
formatValue,
])

const handleMouseHover = useCallback(
Expand Down
75 changes: 75 additions & 0 deletions 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<RawDatum extends BarDatum> {
data: BarTotalsData[]
springConfig: Partial<AnimationConfig>
animate: boolean
layout?: BarCommonProps<RawDatum>['layout']
}

export const BarTotals = <RawDatum extends BarDatum>({
data,
springConfig,
animate,
layout = svgDefaultProps.layout,
}: Props<RawDatum>) => {
const theme = useTheme()
const totalsTransition = useTransition<
BarTotalsData,
{
x: number
y: number
labelOpacity: number
}
>(data, {
keys: barTotal => barTotal.key,
from: barTotal => ({
x: layout === 'vertical' ? barTotal.x : barTotal.animationOffset,
y: layout === 'vertical' ? barTotal.animationOffset : barTotal.y,
labelOpacity: 0,
}),
enter: barTotal => ({
x: barTotal.x,
y: barTotal.y,
labelOpacity: 1,
}),
update: barTotal => ({
x: barTotal.x,
y: barTotal.y,
labelOpacity: 1,
}),
leave: barTotal => ({
x: layout === 'vertical' ? barTotal.x : barTotal.animationOffset,
y: layout === 'vertical' ? barTotal.animationOffset : barTotal.y,
labelOpacity: 0,
}),
config: springConfig,
immediate: !animate,
initial: animate ? undefined : null,
})

return totalsTransition((style, barTotal) => (
<animated.text
key={barTotal.key}
x={style.x}
y={style.y}
fillOpacity={style.labelOpacity}
style={{
...theme.labels.text,
pointerEvents: 'none',
fill: theme.text.fill,
}}
fontWeight="bold"
fontSize={theme.labels.text.fontSize}
fontFamily={theme.labels.text.fontFamily}
textAnchor={layout === 'vertical' ? 'middle' : 'start'}
alignmentBaseline={layout === 'vertical' ? 'alphabetic' : 'middle'}
>
{barTotal.formattedValue}
</animated.text>
))
}

0 comments on commit d90a326

Please sign in to comment.